{"openapi":"3.1.0","info":{"title":"Nexus Exchange API","description":"Perpetual futures exchange with HMAC API key authentication for trading endpoints, tiered rate limiting, and real-time WebSocket streaming across 32 markets.\n\n## Networks\n\nThe API is served per **network**, and the network is the *host* — not a path, not a header, and not a release channel. Two public networks:\n\n| Network | REST base | WebSocket | Funds |\n|---|---|---|---|\n| **Testnet** | `https://api.testnet.nexus.xyz/v1` | `wss://api.testnet.nexus.xyz` | play — synthetic USDX from the faucet, no real-world value |\n| **Mainnet** | `https://api.nexus.xyz/v1` | `wss://api.nexus.xyz` | **real** — USDX bridged from Ethereum Mainnet |\n\nWebSocket paths are identical on every host: market data at `…/stream`, authenticated at `…/ws?token=…`. `x-nexus-networks` is the machine-readable form of this table and the single place to copy the map from; it also carries `local` (`http://localhost:9090`) as a developer convenience.\n\nMainnet is the real-funds exchange running against **Ethereum Mainnet via the USDX bridge** — it is not a Nexus L1 chain. Testnet has a faucet (`POST /faucet`, `POST /account/credit`); mainnet has none, and collateral arrives through the bridge instead.\n\n**Mainnet is deliberately off-pattern.** It takes the bare `api.nexus.xyz`, so `api.{network}.nexus.xyz` is wrong for exactly one network — the real-funds one. Interpolating the network into the host resolves in dev, staging and testnet and fails only where it cannot be rehearsed. Copy the explicit map instead, with mainnet as a named case.\n\n**Credentials do not cross networks.** Session tokens, HMAC API keys, and agent keys are per-network; a key minted on testnet cannot sign for mainnet. Signatures, nonces, and agent registrations are network-scoped too — the EIP-712 signing domain differs per network (see `POST /agents/register`), which is what makes an action signed for one network invalid on the other. Never replay one network's signed payloads against another, and if you cannot determine the signing domain for the network you are on, refuse to sign rather than guess.\n\n**No implicit default, and no blanket redirect.** There is no default network — select one explicitly. `servers[0]` is still the legacy gateway base `https://exchange.nexus.xyz/api/exchange`, which serves **testnet**, so existing clients keep working unchanged; when it retires, its traffic belongs on `api.testnet.nexus.xyz`, never on the bare `api.nexus.xyz`. Pointing the legacy host at mainnet would silently move play-funds clients onto real funds.\n\n### Base paths and the signed path\n\nOpenAPI server selection uses an operation override first, then a path-item override, then the document servers. The 38 `/api/v1/*` path items override the document servers with public testnet host root `https://api.testnet.nexus.xyz` (or `http://localhost:9090` for local development). Append the full operation path: `/api/v1/orders` therefore resolves to `https://api.testnet.nexus.xyz/api/v1/orders`. Unversioned paths inherit the document servers; explicitly selecting base `https://api.testnet.nexus.xyz/v1` plus `/orders` produces `/v1/orders`. A manually configured client that uses that same base for `/api/v1/orders` produces `/v1/api/v1/orders`, which the public testnet chart also supports. The chart retains `/indexer` as a compatibility transport prefix. This contract and chart do not establish live deployment readiness: verify the selected URL before migrating a client.\n\nSign the logical path the indexer authenticates, excluding transport prefixes removed by the gateway. On public testnet, `/v1/orders` and the compatibility `/indexer/orders` both reach `/orders` and sign `/orders`; direct `/api/v1/orders` reaches and signs `/api/v1/orders` unchanged. Existing SDKs whose logical path already includes `/api/v1` can use the host root as their direct base; using `/v1` as that base sends `/v1/api/v1/orders`, strips only the outer `/v1`, and still signs `/api/v1/orders`. The signature never includes the host or query string in its path field; the canonical query and exact body bytes remain separate signed fields. Changing a deployment base does not automatically change the signed logical path: verify its rewrite contract. Never add a second accepted canonical signature as a workaround for an incorrect base.\n\n### Discovering targets at runtime\n\n`/metadata` — served by the edge, not an operation in this contract — publishes the network it is serving, the per-network REST and WebSocket targets, and the EIP-712 signing domain, so clients and agents can discover targets rather than hardcode them. The `Metadata` schema documents the payload. Everything beyond `current_api_version` and `min_api_version` is optional: an older edge may omit it, in which case fall back to `x-nexus-networks` — except the signing domain, which must never be guessed.\n\n## API keys are bound to one network\n\nAn API key is valid on exactly one network. There is no shared key store across networks, so a testnet key cannot authenticate against the real-funds mainnet instance and a mainnet key cannot authenticate against testnet. The rule to internalize is narrower — and simpler — than “one key per network”: **a credential belongs to the host that minted it.**\n\n**Calling `POST /keys` is the binding decision, and the host is its only input.** There is no network parameter in the request and none in the response: the key belongs to the network of the instance that minted it. Mint against `api.testnet.nexus.xyz` and you hold a play-funds key; mint against `api.nexus.xyz` and you hold a real-funds one. `POST /auth/login` is scoped the same way, so sign in and mint on the host you intend to trade against — a session token borrowed from another network is refused at `POST /keys` like any invalid token.\n\n**The guarantee does not come from the signature.** The HMAC canonical string has no network component (see `hmacAuth`), so a signed request is byte-identical on every host; what refuses it elsewhere is that the key is absent from that host's key store entirely. Refusal happens where the store is loaded rather than per request, which is what makes this a structural property of the deployment rather than a check some future code path could skip. Do not read the network's absence from the canonical string as licence to replay a signed payload against another host.\n\n**A wrong-network key is deliberately indistinguishable from one that never existed.** It is refused with the same opaque `401` as an unknown key, with no hint that the key is live elsewhere, so that a guessed key id cannot be confirmed against the other network. Two consequences for clients:\n\n- **Do not retry, and never fail over to another host.** The refusal is permanent for that key on that host: no backoff changes it, and re-sending the request to the other network is the one “recovery” that must never be automated — it points a client that believed it was on play funds at real ones.\n- **Do not read it as key loss.** A `401` straight after a base-URL change is almost always the credential and the host disagreeing, not a deleted key; check which host minted the key before re-minting or escalating. The same `401` is equally consistent with a stale timestamp or a signature computed over the old path (see “Base paths and the signed path”), which is a further reason not to infer the cause from the status alone.\n\n**Revocation is scoped the same way.** `GET /keys` lists only the keys of the host serving the request, and `DELETE /keys/{key_id}` deletes only there. A key missing from that list, or a `404` from the delete, says nothing about any other network and is never evidence that a key has been retired — to retire a leaked key, revoke it on the host that minted it and confirm the revocation there.\n\n**Keys minted before network stamping existed** are adopted on first load by testnet and local instances and refused outright by mainnet: an unstamped key proves nothing about where it came from, and a real-funds instance will not assume. A long-lived key that stops working on mainnet and nowhere else needs replacing with one minted against mainnet.\n\nTreat network scoping as the *weakest* isolation you are promised, never the strongest. Keys are guaranteed not to cross networks; nothing here promises they are shared between two deployments of the same network. The minting host stays the only host a credential is known to be valid on.\n\n## Authentication\n\nOrdinary account-to-account USDX transfers use `ownerSignature`: a recoverable secp256k1 owner signature over the canonical method, path, query, exact body, timestamp and nonce. Enrollment and payment bodies also bind the deployment domain. These operations require the account owner; session tokens, HMAC API keys and agent credentials do not substitute for this signature.\n\nEvery operation states its auth posture, and the statement is machine-readable. There is no global `security`, so each one declares its own: `hmacAuth` (HMAC API key — trading and account reads), `bearerAuth` (a session token from `POST /auth/login`, for API-key management only), `adminAuth`, or `ownerSignature` (ordinary USDX transfers). An operation declaring `security: []` is genuinely public.\n\n**Three mutating operations are authorized by a wallet signature carried in the request body**, which no OpenAPI security scheme can express: `POST /auth/login` (EIP-191 `personal_sign`), `POST /agents/register` and `POST /withdrawals` (EIP-712 typed data). They take no `X-API-Key` and no session token — the server recovers the signer from the body and refuses anything that does not recover to the claimed wallet with `401`. API-key headers sent to them are ignored; they do not substitute for the signature.\n\nTheir `security` is therefore **omitted** rather than `[]`, because neither value is true: they are not public, and they use no scheme this document defines. Omitted is indistinguishable from public to a generic reader or code generator, so **`x-nexus-auth` is the marker that tells the two apart** — an operation carrying it is authenticated, by a mechanism described here in prose and nowhere in `securitySchemes`. Its one value today is `wallet-signature`. Read it before generating a client: an unsigned call to one of these is a `401`, not a public endpoint, and the failure looks like broken auth rather than a missing signature.\n\n`GET /ws` is a separate case and carries no marker: its token is an ordinary query parameter (`?token=…` from `POST /ws-tokens`) documented as such, not an auth scheme.\n\n## Request conventions\n\nEvery official client (SDK, CLI, MCP server) sends two advisory headers on **every** request:\n\n- **`X-Nexus-Api-Version`** — the released spec tag the client was compiled/pinned against, e.g. `v0.7.0` (format `vMAJOR.MINOR.PATCH`, matching the client's `.api-version`). Lets the edge attribute traffic to a spec version and enables future compatibility handling.\n- **`User-Agent`** — `nexus-exchange-<lang>/<version>`, e.g. `nexus-exchange-rs/0.5.1`, for per-client usage metering.\n\nBoth are **advisory and optional**: the server accepts requests when a header is missing, malformed, or names an unknown tag — it never uses them for authentication, authorization, or routing, and treats a missing value as an unknown/legacy client. They are **not** part of the HMAC canonical signing string, so they are unauthenticated and can be altered in transit; never trust them for authentication, authorization, or access control. They are for observability and usage metering only. See the reusable `NexusApiVersion` and `UserAgent` header parameters under `components.parameters`.\n\n## Cursor pagination\n\nFive list operations page by cursor: `/fills`, `/orders/history`, `/positions/closed`, `/account/equity-history` and `/markets/{market_id}/trades` — and the `/api/v1` spellings of the same five. No other operation accepts `cursor` or returns `X-Next-Cursor`.\n\n**The body stays a bare array.** Pagination state rides only in the `X-Next-Cursor` response header, so a client that ignores the header simply gets the first page. A browser can only read that header when the response exposes it via CORS, which the gateway does.\n\n**Walking a list.** Request the first page without `cursor`. If the response carries `X-Next-Cursor`, send that value back as `cursor` for the next page; when it is absent, the walk is complete.\n\n**Stop on the absent header, never on a short page.** The header is present if and only if at least one further record remained when the response was built, so a last page that exactly fills `limit` carries no header rather than being followed by an empty one. The converse is the part clients get wrong: a page can come back *shorter* than `limit` and still carry a cursor, because a page boundary is never placed inside a run of records that share one ordering key.\n\n**A page can also exceed `limit`.** A cursor names a position in the list's sort order, not an offset, so a run of records sharing one ordering key is returned whole rather than split — even when the run is longer than `limit`. The overrun is bounded by the length of that run. This is only reachable where the ordering key can repeat: `/positions/closed`, keyed on closed-at millisecond plus market, and `/account/equity-history`, keyed on the sample millisecond with no tiebreaker. The other three tiebreak on the record `id`.\n\n**`limit` bounds a page, not the walk.** Every request is served from the operation's retained window, and that window is the size of its documented `limit` maximum: 1,000 fills, 500 order-history records, 200 closed positions, 720 equity points, 10,000 trades. A complete walk therefore returns at most that many records however small a `limit` you page with — paging bounds response size, it does not reach further back. A `limit` outside the documented range is clamped rather than refused: above the maximum it serves the maximum, below 1 it serves 1.\n\n**A walk is not a snapshot.** The window is re-read on every request, and these are bounded buffers that drop their oldest records. On the four newest-first lists, records arriving mid-walk sort ahead of your cursor and are not returned by it; on the oldest-first `/account/equity-history` they sort behind it and are. Records dropped before the walk reaches them are not returned at all. What resumption does guarantee is that it never repeats or reorders what it has already returned, and never skips a record that was in the window when the walk began and is still retained — including when the record the cursor itself names has since been dropped, in which case the walk resumes at the nearest surviving boundary rather than restarting.\n\n**A cursor is a position, not a handle.** No server-side state is kept for one, so there is nothing to expire — and equally nothing binding it to an operation or an account. A cursor issued by one list is decoded and interpreted against whichever list you send it to, so use it only with the operation that issued it. Account scope is applied before pagination, so a cursor can never reach another account's records. The token is a reversible encoding of a sort position rather than a signed or encrypted one: treat it as data, not as a credential, and expect a tampered token to resume from wherever the position it decodes to falls. A token the server cannot decode at all is served as a first page rather than refused.\n\n## Rate limits\n\nEvery authenticated request is charged against a **token bucket that refills continuously at your tier's per-second rate**, with a capacity of exactly one second of tokens — so the sustained rate and the burst are the same number, and `remaining` never exceeds `limit`.\n\n**`limit` is a budget of weight per second, not a count of requests.** Most requests cost one unit. Heavy aggregate and history reads cost **5**, because each folds or scans a large per-account buffer: `/account/summary`, `/fills`, `/orders/history`, `/account/portfolio-history` and `/positions/pnl`. A batch order submit costs `1 + floor(order_count / 40)`, so up to 40 orders cost the same as one and every further 40 adds a unit; a batch the server cannot parse is charged the base unit rather than rejected over weighting. A Pro caller at `limit: 20` can therefore make 20 ticker reads per second — or 4 `/fills` reads. Pace on the weight, not on the request count. A single request's cost is capped at one second of tokens, so an oversized batch can never be permanently unsatisfiable.\n\nOperations costing more than one unit carry **`x-nexus-rate-limit-weight`** (an integer), and those whose cost depends on the request body also carry **`x-nexus-rate-limit-weight-formula`**. **Absence of the marker means weight 1.** They are the machine-readable form of the paragraph above, for client-side limiters; like every ceiling here they are current configuration rather than a frozen contract.\n\n### Resource classes\n\nThree budgets, independent of one another. Spending one does not spend the others, and each refuses separately:\n\n| Class | Covers | Charged against |\n|---|---|---|\n| **Requests** | every REST operation that is not an order write | the per-key and per-owner request buckets |\n| **Trading actions** | `POST` and `PATCH` under `/orders`, marked `x-nexus-rate-limit-class: trading` | a dedicated order bucket of the same per-second size |\n| **Cancellations** | `DELETE` under `/orders`, also marked `x-nexus-rate-limit-class: trading` | a **separate** cancel bucket, again of the same per-second size |\n| **WebSocket control plane** | connections, subscriptions and inbound client frames | per-tier ceilings, documented on `/ws` |\n\nAn order write is charged to the trading bucket **instead of** the request bucket, not in addition — so order flow cannot be starved by polling, nor polling by order flow.\n\n**Cancellations are a fourth budget, and never share a token with submission.** A `DELETE` on the order surface — cancel-one, cancel-all, or cancel-by-market — is charged to the cancel bucket instead of the order bucket, so a key that has spent its entire submission allowance placing orders still has a full, untouched allowance for pulling them back. This is the one asymmetry in the model, and it is deliberate: submission can wait, risk reduction cannot, and a limiter that refuses a cancel while a position runs against you has turned a fairness control into a loss. Two things follow. Never infer your cancel headroom from a submission `429` — the `order` bucket being empty says nothing about the `cancel` one. And do not treat cancellation as free: it is a separate bucket, not a bypass, bounded by the same per-second tier rate as the others, so a cancel loop can still `429` — with `bucket: cancel`, which is the only refusal that actually means your cancel channel is saturated. Amend (`PATCH`) is charged as submission, not cancellation: the method does not say whether an amend reduces or increases exposure. If you need the guarantee, cancel. That independence is a property of the per-owner tiers and does **not** extend to `Unlimited`, which draws reads and order writes from one shared per-IP bucket — see “Tiers and current ceilings”. The consequence worth internalizing: a healthy `x-ratelimit-remaining` on your last read says nothing about your order-placement headroom — read `/account/rate-limit`'s `buckets.order` for that, which reports each budget separately under the same labels a `429` names in `bucket`.\n\n`POST /orders/preview` is a trading action as well, which catches people out: it is a write on the `/orders` surface and costs a trading-class unit exactly as placing an order does. Previewing before every order therefore halves your effective placement rate — budget two trading-class charges per order placed that way, or skip the preview once you already know the sizing.\n\nA caller presenting an HMAC key passes a per-key bucket and then the per-owner bucket for its tier; the effective ceiling is whichever binds first. `/account/rate-limit` reports that minimum in its top-level fields, and each of the two buckets individually under `buckets`; polling it is free — it is the one operation that consumes no tokens.\n\n### Headers\n\n- On every authenticated response: **`x-ratelimit-limit`** and **`x-ratelimit-remaining`**.\n- On a `429` only, additionally: **`x-ratelimit-reset`** (unix seconds) and **`retry-after`** (seconds, never below 1). Do not expect either on a success — a client that reads `x-ratelimit-reset` off a 2xx will read nothing.\n\n**`remaining` and `retry-after` are deliberately in different units.** `remaining` is expressed in unit-cost requests — `x-ratelimit-remaining: 10` means ten weight-1 requests *or* two heavy ones — while `retry-after` is derived from the weighted cost of the request that was refused. A limiter that reads `remaining` as “requests of the kind I am about to send” will over-send on heavy endpoints and 429 itself.\n\nA `429` body is `{\"code\": \"RATE_LIMIT_EXCEEDED\", \"message\": …, \"bucket\": …, \"tier\": …}`. Branch on `code`, and on **`bucket`** to learn which pool bottlenecked: `key` (per-API-key), `owner` (per-account requests), `order` (the trading bucket — submission and amend), `cancel` (the separate cancellation bucket) `ip` (the public per-IP bucket) or `login` (the dedicated, tighter per-IP bucket for `POST /auth/login`). The same value rides on the `x-ratelimit-bucket` header, so a proxy or a retry wrapper can read it without parsing the body. Because the classes above are independent, a refusal on one is not a reason to pause the others — a client that backs the whole connection off a read-budget `429` starves its own order flow. The `message` says the same thing in prose and remains a diagnostic: its wording is not stable and must not be matched programmatically.\n\nUnlike the `403` jurisdiction refusals, a `429` **is** retryable: honour `retry-after` and back off. Pacing off `x-ratelimit-remaining` beats discovering the ceiling by hitting it.\n\n### Tiers and current ceilings\n\nTiers are multipliers on one model, not different models. These numbers are per-deployment configuration and not part of this contract — read `/account/rate-limit` rather than hardcoding them:\n\n| Tier | Requests | Trading actions | WS connections | WS subscriptions | WS inbound frames |\n|---|---|---|---|---|---|\n| `Pro` | 20/s | 20/s | 5 | 50 | 10/s |\n| `MarketMaker` | 2,000/s | 2,000/s | 100 | 1,000 | 50/s |\n| `Unlimited` | per-IP, 50/s | per-IP, 50/s — the same bucket as reads | exempt | exempt | exempt |\n\n**Cancellations get their own budget at the Trading actions rate** — a further 20/s for `Pro`, 2,000/s for `MarketMaker` — rather than a column of their own above, because the two numbers are always equal by construction. `Unlimited` is the exception, as with every other class: its cancels are charged to the same per-IP bucket as its reads and order writes, so on that tier the cancel guarantee does not hold.\n\n`Unlimited` is for gateway keys that multiplex many users; it is never assigned to a trading account, and its traffic is bucketed per client IP instead. Order writes on that tier are **not** exempt from rate limiting: they skip the dedicated trading bucket and are charged to the same per-IP bucket as reads, so the class independence above does not hold there — a gateway's order flow can be crowded out by its own polling, on the tier whose traffic mix is least predictable. The WS ceilings it is exempt from are the **per-account** ones; the per-IP connection cap described on `/ws` still applies. Requests whose client IP cannot be resolved are not admitted unthrottled — they share one strict bucket, so an unresolvable origin degrades to a low ceiling rather than to no ceiling.\n\n**Budgets are per-network**, because each network is its own deployment: spending on testnet does not reduce mainnet headroom, and neither does the reverse. Credentials do not cross networks either — see “Networks”.\n\n## API version support\n\nThe version identifier is the released spec tag of this contract (`vMAJOR.MINOR.PATCH`) — the tag every official SDK pins and reports in `X-Nexus-Api-Version`. The edge publishes the versions it accepts at the `/metadata` endpoint — served by the edge, not an operation in this contract — returning `current_api_version` (latest tag served) and `min_api_version` (oldest tag still accepted), so clients and agents can discover the support window programmatically.\n\nPre-1.0 (`v0.x.y`), breaking changes are frequent and `min_api_version` may advance with any breaking release; a released tag stays supported for at least 14 days after the release that supersedes it, and this window widens after 1.0. A request whose `X-Nexus-Api-Version` names a recognized tag older than `min_api_version` receives a machine-readable `426 Upgrade Required` (error code `api_version_unsupported`) with a link to the current spec, so tooling and agents can detect the skew and upgrade. A missing, malformed, or unknown version header is treated as an unknown/legacy client and is not blocked. Because the header is unauthenticated (excluded from the HMAC canonical string), this version gate is a compatibility courtesy, not a security control — authentication and authorization never depend on it, and spoofing the header only relaxes the gate.\n\n## Jurisdiction restrictions\n\nTwo independent geo controls can refuse a request with `403`. They are told apart by the machine-readable `code` in the body and the identical `x-nexus-block-reason` response header, never by the status code alone.\n\n- **`RESTRICTED_JURISDICTION`** — the venue's published sanctions list. It applies to **every** operation in this contract, reads included, so it is not repeated per-operation below. Treat it as possible on any request.\n- **`US_RESTRICTED`** — state-changing operations only. Market data, order-book, account and position reads, the WebSocket stream, order cancellation and withdrawals are unaffected: this is a read-only posture, not a block. The operations it can refuse declare a `403` explicitly.\n- **`GEO_UNRESOLVED`** — a state-changing operation could not be attributed to a country (no usable client address), so it failed closed. This is **not** a claim about where the caller is; it is an operational signal, kept distinct precisely so that a proxy misconfiguration is never reported as \"you are in the United States\".\n\n**Do not retry any of them.** All three are permanent for the caller's origin: no amount of backoff changes the outcome, and a retry loop against a permanent condition is the failure mode this taxonomy exists to prevent. That is what separates a `403` here from `401` (re-authenticate, then retry) and `429` (back off, then retry). Surface it to the operator instead — and for `GEO_UNRESOLVED`, surface it as a configuration problem rather than a jurisdiction one.\n\nTreat an unrecognized `code` on a `403` that carries `x-nexus-block-reason` the same way: refused, permanent, not retryable. Codes are added additively, so a closed client-side enum will go stale.\n\nWhich controls are in force, and over which countries, is per-deployment configuration and is not part of this contract; testnet deployments are not subject to the write restriction. Write clients should handle the error regardless of which deployment they are pointed at.\n\n**Over WebSocket** there is no refusal frame, because there is no state-changing WebSocket operation: the inbound `/ws` envelope accepts `subscribe` and `unsubscribe` only, and order submission is REST-only. If a state-changing inbound op is ever added it must carry these same codes so both surfaces agree.","version":"0.9.57"},"servers":[{"url":"https://exchange.nexus.xyz/api/exchange","description":"Testnet — legacy gateway base, and the current default. Play funds: balances are synthetic USDX with no real-world value. Every released SDK targets this base today, so it stays first and remains the default a generator picks; it is transitional and will be removed in a deliberate breaking release once the per-network hosts below are live. Because it serves **testnet**, its traffic migrates to `https://api.testnet.nexus.xyz/v1` — never to the bare `api.nexus.xyz`, which is real funds. See `x-nexus-networks` for the authoritative network → target map."},{"url":"https://api.testnet.nexus.xyz/v1","description":"Public testnet (play funds) — /v1 transport base for unversioned paths. Requires the public testnet route promotion; verify deployment before use."},{"url":"https://api.nexus.xyz/v1","description":"Mainnet — **REAL FUNDS**. Collateral is USDX bridged from Ethereum Mainnet; orders placed here move real money, and there is no faucet. The durable per-network base. Listed after testnet on purpose, so nothing that reaches for \"the first https server\" lands on the real-funds target. Not resolvable yet: DNS/TLS is a separate infra change."},{"url":"http://localhost:9090","description":"Local development — the indexer served directly. Not a public network; whatever your local instance holds."}],"x-nexus-networks":{"description":"Authoritative network → target map. The public network axis is **testnet** (play funds) vs **mainnet** (real funds); `local` is a developer convenience, not a public network. The network is carried in the host, not in the path, and each host is its own origin terminating its own TLS and WebSocket upgrades.\n\n**Never derive a host by interpolating the network name.** Mainnet is deliberately off-pattern — `api.nexus.xyz`, not `api.mainnet.nexus.xyz` — so `api.{network}.nexus.xyz` resolves for every environment that can be tested and fails only on real funds, which is the one environment that cannot be rehearsed. Consumers (SDK network enums, CLI config, CORS allowlists, deploy env blocks) must copy this map with mainnet as a named case.\n\n**Credentials never cross networks.** Session tokens, HMAC API keys, and agent keys are minted per network and are invalid on any other, so a key leaked or misconfigured on testnet cannot sign for real funds. A client that switches network must also switch credentials, and must never carry a signature, nonce, or agent registration across networks. The binding is made by the host: `POST /keys` takes no network parameter, so whichever host mints a key decides where it is valid, and listing and revocation are scoped to that host too. Elsewhere the key is refused as an ordinary opaque `401`, indistinguishable from an unknown key — see “API keys are bound to one network” in the API description.\n\n**Operation availability.** An operation carrying `x-nexus-network-availability` is served only on the networks it lists — today just the synthetic-funding operations, which are testnet-only. No marker means the operation is not network-restricted.\n\n**Non-public environments are deliberately absent.** Internal mainnet, staging, and devnet have no decided public API host; nothing here should be extrapolated to them. Treat a network identifier you do not recognize as **real funds** — the fail-safe direction is to require confirmation, never to assume play money.","networks":{"testnet":{"label":"Testnet","funds":"play","faucet":true,"host":"api.testnet.nexus.xyz","rest_base":"https://api.testnet.nexus.xyz/v1","ws_url":"wss://api.testnet.nexus.xyz","ws_market_data_url":"wss://api.testnet.nexus.xyz/stream","ws_authenticated_url":"wss://api.testnet.nexus.xyz/ws","signing_domain":{"name":"Nexus Exchange","version":"1","chain_id":null},"description":"Play funds: balances are synthetic USDX credited by the faucet (`POST /faucet`, `POST /account/credit`) and carry no real-world value. The safe target for integration work and CI. Served today by the legacy base `https://exchange.nexus.xyz/api/exchange`; that traffic migrates here."},"mainnet":{"label":"Mainnet","funds":"real","faucet":false,"host":"api.nexus.xyz","rest_base":"https://api.nexus.xyz/v1","ws_url":"wss://api.nexus.xyz","ws_market_data_url":"wss://api.nexus.xyz/stream","ws_authenticated_url":"wss://api.nexus.xyz/ws","signing_domain":{"name":"Nexus Exchange","version":"1","chain_id":null},"description":"**Real funds.** Collateral is USDX bridged from Ethereum Mainnet (see the `Bridge` tag) — there is no faucet and no synthetic credit, and every order moves real money. Mainnet is the real-funds exchange running *against Ethereum Mainnet via the USDX bridge*; it is not a Nexus L1 chain, so never assume a Nexus L1 chain id here."},"local":{"label":"Local","funds":"play","faucet":true,"host":"localhost:9090","rest_base":"http://localhost:9090","ws_url":"ws://localhost:9090","ws_market_data_url":"ws://localhost:9090/stream","ws_authenticated_url":"ws://localhost:9090/ws","signing_domain":{"name":"Nexus Exchange","version":"1","chain_id":null},"description":"A locally run indexer. Not a public network and not a deployment target — never a fallback when a public host fails to resolve, since silently succeeding against localhost hides a misconfigured client."}},"signing_domain_note":"Each entry's `signing_domain` is the EIP-712 domain for that network, deliberately spelled the same as `/metadata`'s `signing_domain` and the `SigningDomain` schema, so one name means one thing across the static map, the runtime payload, and generated clients.\n\n`chain_id: null` means this document does not publish the value, not that the value is zero. The signing domain is per-network and server-authoritative: read `signing_domain` from `/metadata` (see the `Metadata` schema) for the network you are connected to. A client that cannot obtain a `chain_id` must **refuse to sign** rather than guess or default — a wrong domain either fails verification or, worse, produces a signature valid on a different network. `name` and `version` are the values this contract has always documented for `POST /agents/register`; `/metadata` remains authoritative for all three fields."},"tags":[{"name":"Authentication","description":"EVM wallet sign-in and API key management"},{"name":"Agents","description":"Agent key registration and management (EIP-712 signed, no session token required)"},{"name":"Markets","description":"Market parameters and summaries"},{"name":"Tickers","description":"24h price statistics","x-ccxt":true},{"name":"Order Book","description":"Live order book depth","x-ccxt":true},{"name":"Trades","description":"Recent trade history","x-ccxt":true},{"name":"Candles","description":"OHLCV candlestick data","x-ccxt":true},{"name":"Funding","description":"Funding rate history"},{"name":"Trading","description":"Order submission and cancellation"},{"name":"Account","description":"Balance and order management"},{"name":"Positions","description":"Open position queries"},{"name":"WebSocket","description":"Real-time streaming via short-lived tokens"},{"name":"Admin","description":"Tier management (requires ADMIN_SECRET)"},{"name":"Bridge","description":"Cross-chain deposits and withdrawal wallets: bridgeable assets, per-account deposit addresses, deposit tracking, and ownership-proven withdrawal wallets (Phase A: USDC/USDX)."}],"paths":{"/auth/login":{"post":{"operationId":"login","tags":["Authentication"],"summary":"Sign in with EVM wallet","description":"Submit an EIP-191 personal_sign signature to receive a session token. The session token is used to create and manage API keys via /keys endpoints. For trading, use HMAC API keys instead. Session tokens expire after 24 hours.","x-nexus-auth":"wallet-signature","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"},"example":{"message":"Sign in to Nexus Exchange","signature":"0x1234...abcd"}}}},"responses":{"200":{"description":"Session created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"},"example":{"token":"a1b2c3d4e5f6...","address":"0xAbCdEf0123456789..."}}}},"401":{"description":"Signature verification failed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"code=INTERNAL_ERROR — the session store write failed (M3.15, ENG-10681: the Postgres-backed session store has no in-memory fallback, so a write failure surfaces here instead of degrading silently)"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Session/credential lifecycle. CCXT takes credentials as constructor config; it never mints them."}},"/keys":{"post":{"operationId":"createApiKey","tags":["Authentication"],"summary":"Create an API key","description":"Create a new HMAC API key for the authenticated wallet. Returns the secret once — it is never stored or shown again. Requires a session token (Bearer) from POST /auth/login.\n\n**The host you call this on decides which network the key is valid on.** There is no network parameter in the request and none in the response: the key belongs to the network of the instance that mints it, and any other host refuses it with an opaque `401` that is indistinguishable from an unknown key. Call this on the host you intend to trade against — `api.testnet.nexus.xyz` for play funds, `api.nexus.xyz` for **real funds** — and mint a separate key per network rather than reusing one. See “API keys are bound to one network” in the API description.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Key created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyResponse"},"example":{"key_id":"nx_a1b2c3d4e5f67890","secret":"deadbeef..."}}}},"401":{"description":"Valid session token required for this host. A session token minted by another network's `POST /auth/login` is refused here like any invalid token."}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Session/credential lifecycle. CCXT takes credentials as constructor config; it never mints them."},"get":{"operationId":"listApiKeys","tags":["Authentication"],"summary":"List your API keys","description":"Returns key IDs and tiers for all keys owned by the authenticated wallet. Secrets are not included. Scoped to the network serving this request: keys minted on another network are not listed here, and their absence is not evidence that they were deleted or that they are inactive on the network that minted them.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The session's API keys.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/KeyInfo"}},"example":[{"key_id":"nx_a1b2c3d4e5f67890","tier":"Pro","label":"my trading bot","created_at_ms":1757350000000},{"key_id":"nx_0f1e2d3c4b5a6978","tier":"Pro","label":null,"created_at_ms":1757360000000}]}}},"401":{"description":"Valid session token required"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Session/credential lifecycle. CCXT takes credentials as constructor config; it never mints them."}},"/keys/{key_id}":{"delete":{"operationId":"deleteApiKey","tags":["Authentication"],"summary":"Delete an API key","description":"Delete a key you own. Cannot delete keys owned by other wallets. Deletion is scoped to the network serving this request. A key minted on another network is invisible here and cannot be revoked here, and the `404` below does not distinguish “no such key” from “not yours” from “live on another network” — so it is never proof that a key has been retired. Revoke a key on the host that minted it, and confirm the revocation there.","security":[{"bearerAuth":[]}],"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Key deleted"},"401":{"description":"Valid session token required"},"404":{"description":"Key not found on this network, or not owned by you. Not proof the key is gone: a key minted on another network is invisible here and is unaffected by this call."}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Session/credential lifecycle. CCXT takes credentials as constructor config; it never mints them."}},"/agents/register":{"post":{"operationId":"registerAgent","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus-specific trading-agent registration. No CCXT equivalent — the closest unified concepts are API-key management, which CCXT deliberately leaves to the venue.","tags":["Agents"],"summary":"Register an agent key","description":"Register a new agent key for your wallet. An agent is an Ethereum-derived keypair that can sign trading requests on your behalf without exposing your main wallet. The registration is authorized by an EIP-712 signature from the wallet that will own the agent — no session token required.\n\nEIP-712 domain: `{ name: 'Nexus Exchange', version: '1', chainId: <per-network chain id> }`. The `chainId` is **network-scoped**: read `signing_domain.chain_id` from `/metadata` for the network you are connected to (see `x-nexus-networks`) instead of hardcoding it, and refuse to sign if you cannot obtain it — a wrong domain either fails verification or produces a signature that is valid somewhere you did not intend. Mainnet is the real-funds exchange on Ethereum Mainnet, not a Nexus L1 chain, so a Nexus L1 chain id is never correct there. Because the domain differs per network, a registration signed for one network does not verify on another; never replay one across networks.\nTyped data type: `RegisterAgent { address agent, uint64 expiresAt, uint64 nonce }`.","x-nexus-auth":"wallet-signature","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRegistrationRequest"},"example":{"wallet":"0xAbCdEf0123456789AbCdEf0123456789AbCdEf01","agent":"0x1234567890AbCdEf1234567890AbCdEf12345678","expires_at":1782000000000,"nonce":1,"signature":"0xdeadbeef..."}}}},"responses":{"200":{"description":"Agent registered","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRegistrationResponse"},"example":{"agent_address":"0x1234567890AbCdEf1234567890AbCdEf12345678","expires_at":1782000000000,"scope":"trade_only","has_referrer":false,"bound_by_this_call":false}}}},"400":{"description":"Bad request: bad_wallet, bad_agent, expiry_out_of_range [1 d, 90 d from now], or invalid_json"},"401":{"description":"signature_invalid or signer_mismatch — the EIP-712 signature did not recover to the claimed wallet"},"409":{"description":"duplicate_agent — the agent address is already registered to this wallet"}}}},"/agents":{"get":{"operationId":"listAgents","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus-specific trading-agent registration. No CCXT equivalent — the closest unified concepts are API-key management, which CCXT deliberately leaves to the venue.","tags":["Agents"],"summary":"List your agents","description":"Returns all non-expired agent keys registered to the authenticated wallet.","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Array of agent records","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentInfo"}},"example":[{"address":"0x1234567890AbCdEf1234567890AbCdEf12345678","expiresAt":1782000000000,"registeredAt":1779000000000,"label":"my-bot"}]}}},"401":{"description":"HMAC authentication required"}}}},"/agents/{address}":{"delete":{"operationId":"revokeAgent","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus-specific trading-agent registration. No CCXT equivalent — the closest unified concepts are API-key management, which CCXT deliberately leaves to the venue.","tags":["Agents"],"summary":"Revoke an agent","description":"Immediately revoke an agent key. Any in-flight requests signed by the revoked agent will be rejected after this call returns.","security":[{"hmacAuth":[]}],"parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string"},"description":"Agent address to revoke (0x-prefixed)"}],"responses":{"200":{"description":"Agent revoked"},"401":{"description":"HMAC authentication required"},"404":{"description":"Agent not found or not owned by you"}}}},"/ws-tokens":{"post":{"operationId":"createWsTokenLegacy","tags":["WebSocket"],"summary":"Mint a WebSocket token (legacy)","description":"Legacy endpoint. Prefer POST /ws/token which supports both HMAC keys and registered agents. Returns a short-lived (60s), single-use token. **`GET /stream` no longer consumes it** (ENG-3128 removed token auth there), and `GET /ws` takes a token from `POST /ws/token` instead — so nothing on the current contract requires a token from this endpoint. Retained for clients that still call it; new integrations should use `POST /ws/token` with `GET /ws`.","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"A minted WebSocket authentication token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WsTokenLegacyResponse"},"example":{"token":"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6"}}}},"401":{"description":"HMAC authentication required"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"WebSocket transport and token minting. Covered by the CCXT Pro channel map, not by a REST unified method."}},"/ws/token":{"post":{"operationId":"createWsToken","tags":["WebSocket"],"summary":"Mint a WebSocket token","description":"Returns a short-lived (60s), single-use token bound to the authenticated account. Pass it as `?token=TOKEN` when upgrading to `GET /ws`. Supports HMAC keys, registered agent keys, and session tokens (Bearer). The token encodes the account identity so per-account channels (orders, fills, positions, balances, liquidations) are automatically scoped to the connected wallet.","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Token minted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WsTokenResponse"},"example":{"token":"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6","expires_at":1757350060000}}}},"401":{"description":"Authentication required"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"WebSocket transport and token minting. Covered by the CCXT Pro channel map, not by a REST unified method."}},"/markets":{"get":{"tags":["Markets"],"summary":"List all markets","operationId":"fetchMarkets","description":"Returns market parameters for all perpetual futures markets including tick size, lot size, margin rates, and maximum leverage.\n\nPublic (ENG-11484): no credentials. This is reference data with no account scope, and every client rounds prices and sizes against it, so a client that cannot read it cannot place a valid order. It was previously declared `hmacAuth` while the indexer routed it nowhere, so it fell through to the authenticated catch-all — the same mismatch ENG-4848/ENG-5187 fixed for `GET /ready`. It appeared to answer unauthenticated only because the browser gateway proxy signs unlisted read paths with its own key; a client talking to an indexer directly got 401.","x-ccxt-method":"fetchMarkets","security":[],"responses":{"200":{"description":"Array of market parameters","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Market"}},"example":[{"id":"BTC-USDX-PERP","base":"BTC","quote":"USDX","tick_size":"0.5","lot_size":"0.001","min_order_size":"0.001","max_order_size":"100","initial_margin_rate":"0.05","maintenance_margin_rate":"0.025","max_leverage":20,"max_open_interest_notional":null,"price_band_bps":500}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/markets/summary":{"get":{"operationId":"fetchMarketsSummary","tags":["Markets"],"summary":"Market summaries with volume","description":"Returns last trade price, 24h volume, and trade count for all markets.","security":[],"responses":{"200":{"description":"Volume and price summaries for all markets.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MarketSummary"}},"example":[{"market_id":"BTC-USDX-PERP","last_trade_price":48850,"volume_24h":19530020.08,"trade_count":45230}]}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Market summary aggregate. `fetchTickers` is the CCXT-shaped equivalent and is mapped."}},"/markets/{market_id}/ticker":{"get":{"tags":["Tickers"],"summary":"Get ticker for a market","operationId":"fetchTicker","x-ccxt-method":"fetchTicker","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"}],"responses":{"200":{"description":"Current ticker for the market.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticker"},"example":{"symbol":"BTC-USDX-PERP","timestamp":1776033911836,"datetime":"2026-04-12T22:45:11.836Z","high":50500,"low":49200,"bid":50100.5,"bidVolume":1.4,"ask":50102,"askVolume":0.8,"open":49800,"close":50100,"last":50100,"change":300,"percentage":0.602,"baseVolume":1250.5,"quoteVolume":62525000,"markPrice":50101.5,"indexPrice":null,"info":{}}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"404":{"description":"Market not found"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/tickers":{"get":{"tags":["Tickers"],"summary":"Get tickers for all markets","operationId":"fetchTickers","x-ccxt-method":"fetchTickers","security":[],"responses":{"200":{"description":"Object keyed by market_id","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/Ticker"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/markets/{market_id}/orderbook":{"get":{"tags":["Order Book"],"summary":"Get order book","operationId":"fetchOrderBook","x-ccxt-method":"fetchOrderBook","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"}],"responses":{"200":{"description":"Current order book for the market.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderBook"},"example":{"symbol":"BTC-USDX-PERP","bids":[[50100.5,1.4],[50099,2.1]],"asks":[[50102,0.8],[50103.5,1.2]],"timestamp":1776033930898,"datetime":"2026-04-12T22:45:30.898Z","nonce":1651}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"404":{"description":"Market not found"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/markets/{market_id}/trades":{"get":{"tags":["Trades"],"summary":"Get recent trades","operationId":"fetchTrades","x-ccxt-method":"fetchTrades","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":10000},"description":"Trades per page (default 100, capped at the 10,000-trade retained window). Bounds one page, not a cursor walk — see “Cursor pagination”."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Recent trades for the market.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Trade"}},"example":[{"id":"cf72c7f3-4c59-4d3c-85c8-99d92bc1fda7","symbol":"BTC-USDX-PERP","price":50100.5,"amount":0.033,"cost":1653.32,"side":"buy","timestamp":1776033942331,"datetime":"2026-04-12T22:45:42.331Z","takerOrMaker":null,"is_liquidation":false,"info":{}}]}},"headers":{"X-Next-Cursor":{"$ref":"#/components/headers/XNextCursor"}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/markets/{market_id}/candles":{"get":{"tags":["Candles"],"summary":"Get OHLCV candles","operationId":"fetchOHLCV","x-ccxt-method":"fetchOHLCV","description":"Returns candlestick data as arrays: [timestamp, open, high, low, close, volume], ascending by timestamp. Optionally bounded by `startTime` / `endTime`; unbounded, it returns the latest `limit` bars.\n\n**`startTime` sets the paging direction.** With `startTime` given, the **earliest** `limit` bars at or after it are returned, so the standard `ccxt.fetchOHLCV` loop that advances `since` progresses: each request starts where the last one ended. Without `startTime` — the unbounded or `endTime`-only case — the **most recent** `limit` bars in the window are returned, which is what \"the latest bars\" means above and is unchanged. To reconstruct a long history, pass `startTime` and advance it by the timestamp after the last bar received. This operation has no pagination cursor.","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"},{"name":"timeframe","in":"query","schema":{"type":"string","enum":["1s","1m","5m","1h"],"default":"1m"}},{"$ref":"#/components/parameters/CandleStartTime"},{"$ref":"#/components/parameters/CandleEndTime"},{"name":"limit","in":"query","description":"Maximum number of bars to return. A value above the maximum is clamped rather than rejected.\n\nThe maximum is what the server accepts, not a promise of how many bars exist: the effective ceiling depends on the history available for that market and timeframe, so a response may hold fewer bars than requested. Read `x-nexus-candles-truncated` to tell a capped page from a complete one.","schema":{"type":"integer","default":200,"maximum":5000}}],"responses":{"200":{"description":"OHLCV candles for the market.","content":{"application/json":{"schema":{"type":"array","items":{"type":"array","prefixItems":[{"type":"integer","title":"timestamp","description":"timestamp (ms)"},{"type":"number","title":"open","description":"open"},{"type":"number","title":"high","description":"high"},{"type":"number","title":"low","description":"low"},{"type":"number","title":"close","description":"close"},{"type":"number","title":"volume","description":"volume"}]}},"example":[[1776033900000,48062,51903,44992,51903,27.123]]}},"headers":{"x-nexus-candles-truncated":{"$ref":"#/components/headers/XNexusCandlesTruncated"},"x-nexus-candles-coverage-start-ms":{"$ref":"#/components/headers/XNexusCandlesCoverageStartMs"}}},"400":{"$ref":"#/components/responses/InvalidCandleQuery"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/markets/{market_id}/funding":{"get":{"operationId":"fetchFunding","tags":["Funding"],"summary":"Get funding rate history","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"},{"name":"limit","in":"query","schema":{"type":"integer","default":300,"maximum":1000}}],"responses":{"200":{"description":"Funding rate history for the market.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundingSample"}},"example":[{"timestamp":1776033960368,"fundingRate":"0.000000016","premium_index":"0.004192","mark_price":"49756.75","oracle_price":"49549.0"}]}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-method":"fetchFundingRateHistory"}},"/markets/{market_id}/status":{"get":{"operationId":"fetchMarketStatus","tags":["Markets"],"summary":"Get market status and halt info (v0.21)","description":"Returns current market status including halt state from ADL exhaustion. Halted markets reject new orders with ExchangeError::MarketHalted.","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"}],"responses":{"200":{"description":"Market status and halt information.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketStatus"}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"404":{"description":"Market not found"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus risk parameters and per-market halt state. CCXT carries neither in its unified market shape."}},"/markets/{market_id}/adl-events":{"get":{"operationId":"fetchAdlEvents","tags":["Markets"],"summary":"Get ADL settlement history for a market (v0.21)","description":"Returns up to `limit` ADL settlement events for the market, most recent first. Populated when the insurance fund is depleted and auto-deleveraging closes opposite-side positions to absorb bad debt.","security":[{"hmacAuth":[]}],"parameters":[{"$ref":"#/components/parameters/MarketId"},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":1000}}],"responses":{"200":{"description":"ADL settlement history for the market.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdlEventRecord"}}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"ADL (auto-deleveraging) is a Nexus risk mechanism with no CCXT vocabulary."}},"/account/{address}/adl-history":{"get":{"operationId":"fetchAdlHistory","tags":["Account"],"summary":"Get ADL events touching an account (v0.21)","description":"Returns ADL settlements where the specified address was either the bankrupt target or one of the counterparties whose position was closed.","security":[{"hmacAuth":[]}],"parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string"},"description":"Account address (0x-prefixed hex)"},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":1000}}],"responses":{"200":{"description":"ADL events touching the account.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdlEventRecord"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"ADL (auto-deleveraging) is a Nexus risk mechanism with no CCXT vocabulary."}},"/markets/{market_id}/mark-price":{"get":{"operationId":"fetchMarkPrice","tags":["Markets"],"summary":"Get mark price","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"}],"responses":{"200":{"description":"Current mark price for the market.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkPriceResponse"},"example":{"market_id":"BTC-USDX-PERP","mark_price":"50011.60"}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"CCXT models mark price as a FIELD, `Ticker.markPrice`, which our `Ticker` schema already declares: `GET /markets/{market_id}/ticker` is badged fetchTicker and `GET /tickers` fetchTickers, so the datum is already reachable through CCXT's own vocabulary. In ccxt 4.5.76 fetchMarkPrices returns Ticker structures keyed by symbol and fetchMarkPrice derives the single-symbol case from it, so badging this route would promise a Ticker from a response that carries one scalar and a market id. Reachable natively as the implicit publicGetMarketsMarketIdMarkPrice."}},"/markets/{market_id}/risk-params":{"get":{"operationId":"fetchMarketRiskParams","tags":["Markets"],"summary":"Get market risk parameters","description":"Returns per-market risk parameters including margin requirements and maximum leverage. Populated by the indexer's market_params_poller from the engine's market registry.","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"}],"responses":{"200":{"description":"Market risk parameters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketRiskParams"},"example":{"market_id":"BTC-USDX-PERP","max_leverage":20,"initial_margin_rate":"0.05","maintenance_margin_rate":"0.025"}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"404":{"description":"Market not found"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus risk parameters and per-market halt state. CCXT carries neither in its unified market shape."}},"/orders":{"post":{"tags":["Trading"],"summary":"Submit an order","operationId":"createOrder","x-ccxt-method":"createOrder","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"},"examples":{"limit":{"summary":"Plain limit order","value":{"market_id":"BTC-USDX-PERP","side":"Buy","order_type":"Limit","price":"50000","quantity":"0.1","time_in_force":"GTC"}},"stopLimit":{"summary":"Stop-limit — requires trigger_price and a limit price","value":{"market_id":"BTC-USDX-PERP","side":"Sell","order_type":"StopLimit","trigger_price":"48000","price":"47900","quantity":"0.1","time_in_force":"GTC"}},"trailingStop":{"summary":"Trailing stop — market-only, requires trailing_offset_bps","value":{"market_id":"BTC-USDX-PERP","side":"Sell","order_type":"TrailingStop","trailing_offset_bps":250,"quantity":"0.1","time_in_force":"IOC"}},"marketWithSlippageCap":{"summary":"Market order with a server-enforced 50 bp slippage cap","value":{"market_id":"BTC-USDX-PERP","side":"Buy","order_type":"Market","quantity":"0.1","time_in_force":"IOC","max_slippage_bps":50}}}}}},"responses":{"200":{"description":"Idempotent replay: this `client_id` was already accepted, and the body is the order that first request created. Nothing was placed by this request, so branch on the status rather than assuming `201`. `fills` is empty here even if the original order has since traded; read its current state from `GET /orders/{order_id}`.\n\n**Reachable only while the original order is still resting.** The replay answers from the live book, so a fully-filled, cancelled or expired original, and any market/IOC/FOK order that never rested, is answered `409` instead. The duplicate is prevented either way; what differs is whether the original can be handed back inline.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"201":{"description":"Order accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"400":{"description":"Validation error (insufficient margin, invalid tick size, etc.)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"409":{"description":"This `client_id` is already claimed, and the order it created is not currently resting, so it cannot be returned inline (`code: DuplicateClientId`). **This is the expected answer whenever the original is no longer on the book**, not a rare edge: a filled, cancelled or expired order, and anything that never rested, all land here. The message names the order's id; find it in `GET /orders/history`, which retains terminal orders. Do not retry with the same key, since the outcome will not change, and do not re-submit under a new key without first establishing what the original order did.\n\nA `409` also covers the market-lifecycle admission gate, distinguished from the case above by `code`: `MarketHalted` when the market is halted, `MarketReduceOnly` when the market is in reduce-only and the order is neither `reduce_only` nor a liquidation, and `MarketSuspended` when the market is settling or delisted."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-class":"trading"},"get":{"tags":["Account"],"summary":"List open orders","operationId":"fetchOpenOrders","x-ccxt-method":"fetchOpenOrders","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"The account's open orders.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Order"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Trading"],"summary":"Cancel all orders","description":"Charged to the **cancel** budget, which is separate from the order budget that submission and amend draw on (`x-nexus-rate-limit-class: trading` names the class both belong to, not the bucket). A key that has spent its entire submission allowance can still cancel: exhausting one never refuses the other. It is a separate bucket rather than an exemption, so cancelling in a tight loop can still `429` — with `bucket: cancel`, the only refusal that means your cancel channel itself is saturated. See \"Rate limits\" for the full model.","operationId":"cancelAllOrders","x-ccxt-method":"cancelAllOrders","security":[{"hmacAuth":[]}],"parameters":[{"name":"market_id","in":"query","schema":{"type":"string"},"description":"Cancel only orders on this market"}],"responses":{"200":{"description":"The orders that were actually cancelled, in the `Order` shape `GET /orders` serves. A cancel-all that could not reach every market still answers 200 with only the orders it did cancel, so an empty or short array is not proof that nothing rests any more.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Order"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-class":"trading"}},"/orders/batch":{"post":{"operationId":"createOrdersBatch","tags":["Trading"],"summary":"Submit multiple orders","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderRequest"}}}}},"responses":{"201":{"description":"Per-order results, in request order. Returned with status 201 for the batch as a whole even when individual entries failed; inspect each entry's `error`.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderResult"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"429":{"$ref":"#/components/responses/RateLimited"}},"description":"Submit multiple orders in one request. Orders are processed sequentially and non-atomically: an early order consuming margin can cause a later order in the same batch to fail, and per-order failures do not abort the batch. The response array preserves request order with a per-order success or error result.\n\n**Weighted by batch size.** This call costs `1 + floor(order_count / 40)` units of the trading-action budget, so up to 40 orders cost the same as a single order and every further 40 adds a unit. Batching is therefore cheaper than the equivalent individual submits, and the cost of one call is capped at one second of tokens so a large batch can never be permanently unsatisfiable. See “Rate limits” in the API description.","x-nexus-rate-limit-class":"trading","x-nexus-rate-limit-weight":1,"x-nexus-rate-limit-weight-formula":"1 + floor(order_count / 40)","x-ccxt-method":"createOrders"}},"/orders/{order_id}":{"get":{"tags":["Account"],"summary":"Get order by ID","operationId":"fetchOrder","x-ccxt-method":"fetchOrder","security":[{"hmacAuth":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"market_id","in":"query","required":true,"schema":{"type":"string"},"description":"Market the order rests on (required for routing)."}],"responses":{"200":{"description":"The requested order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such order for this account: the id was never placed by this account, or it refers to a terminal order older than the 500-per-account retention window. A 404 is deliberately identical for \"never existed\" and \"not yours\" (ownership masking). It no longer means \"not currently resting\" — recently terminal orders return 200 (ENG-10962)."}},"description":"Resolves any order the account placed: resting orders come from the matching engine's authoritative book state, and recently completed orders (filled, cancelled, rejected, expired) are served from the indexer's terminal-order mirror, so an order that fills instantly can still be fetched by the id its placement returned (ENG-10962). The mirror retains the most recent 500 terminal orders per account — the same window `GET /orders/history` lists — so anything listable is fetchable. Older terminal orders resolve only through `GET /orders/history`."},"delete":{"tags":["Trading"],"summary":"Cancel an order","description":"Charged to the **cancel** budget, which is separate from the order budget that submission and amend draw on (`x-nexus-rate-limit-class: trading` names the class both belong to, not the bucket). A key that has spent its entire submission allowance can still cancel: exhausting one never refuses the other. It is a separate bucket rather than an exemption, so cancelling in a tight loop can still `429` — with `bucket: cancel`, the only refusal that means your cancel channel itself is saturated. See \"Rate limits\" for the full model.","operationId":"cancelOrder","x-ccxt-method":"cancelOrder","security":[{"hmacAuth":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"market_id","in":"query","required":true,"schema":{"type":"string"},"description":"Market the order rests on (required for routing)."}],"responses":{"200":{"description":"The cancelled order, in the `Order` shape `GET /orders` serves.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Order not found"}},"x-nexus-rate-limit-class":"trading"},"patch":{"tags":["Trading"],"summary":"Amend an order","description":"Atomic cancel-replace amend of a resting order: changes the price and/or size in a single operation. At least one of `price` or `size` must be supplied. Liquidation orders are not amendable, and a pre-trade margin check is applied to the projected replacement before it is accepted. That check excludes the reservation still held by the order being replaced, so an amend is sized on the margin the replacement actually adds rather than on the original and the replacement together: repricing a resting order at the same size needs no additional margin, and shrinking one frees margin rather than requiring it.\n\nAn amend does not restart execution. The replacement carries the original's `filled` forward, so `amount` stays the total you asked for and an order can never execute more than that total, across any number of amends: a Buy 5 that has filled 2 has 3 left to execute both before and after a reprice, and the replacement comes back as `PartiallyFilled` rather than `Open`. `size` sets the new TOTAL quantity, fills included, and must be greater than `filled` — a size at or below it is rejected with InvalidAmend; cancel the order instead.","operationId":"editOrder","x-ccxt-method":"editOrder","security":[{"hmacAuth":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"market_id","in":"query","required":true,"schema":{"type":"string"},"description":"Market the order rests on (required for routing)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmendOrderRequest"},"example":{"price":"50100","size":"0.2"}}}},"responses":{"200":{"description":"Amended order (the replacement, with a fresh id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"400":{"description":"Invalid amend (empty body, invalid price/size, order not amendable, or margin breach)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"404":{"description":"Order not found"},"409":{"description":"The market-lifecycle admission gate, evaluated on the replacement before the original is touched: `MarketHalted` when the market is halted, `MarketReduceOnly` when the market is in reduce-only and the replacement is neither `reduceOnly` nor a liquidation, and `MarketSuspended` when the market is settling or delisted. Rejected atomically — the original order is left resting, untouched."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-class":"trading"}},"/account":{"get":{"tags":["Account"],"summary":"Get account summary","operationId":"fetchBalance","x-ccxt-method":"fetchBalance","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Account summary for the authenticated caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountSummary"},"example":{"balance":"100000.00","collateral":"100000.00","equity":"102500.50","available_margin":"85000.00","positions":[]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/account/deposit":{"post":{"operationId":"deposit","tags":["Account"],"summary":"Deposit USDX collateral","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"example":{"amount":"10000","deposit_id":"0xfeed:0","confirmed_at_ms":1757350000000,"block_number":21500000}}},"description":"`amount` alone for a manual/testnet credit. The other three are the internal deposit callback's, sent by eth-watcher and all optional: `deposit_id` (`\"{txHash}:{logIndex}\"`) is the idempotency key — a replay returns the current balance without re-crediting; `confirmed_at_ms` measures credit latency; and `block_number` is the chain block the deposit was observed in, which bounds the engine's deposit-dedup retention window (ENG-14492). Omitting `block_number` is valid and means \"no known block\", which retains that id indefinitely rather than ageing it out."},"responses":{"200":{"description":"The deposit result.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepositResponse"},"example":{"balance":"110000.00"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Testnet-only balance credit and the internal deposit callback. Not client-facing in CCXT terms."}},"/account/margin":{"post":{"operationId":"adjustMargin","tags":["Account"],"summary":"Add or remove isolated margin on an open position","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"example":{"market_id":"BTC-USDX-PERP","amount":"100","direction":"add"}}}},"responses":{"200":{"description":"Updated allocated margin and account collateral after the adjustment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdjustMarginResponse"},"example":{"market_id":"BTC-USDX-PERP","allocated_margin":"350.00","collateral":"9900.00"}}}},"400":{"description":"Invalid amount, position not in isolated mode (MarginModeNotIsolated), or removal breaches the withdrawal floor / exceeds collateral (InsufficientMargin / InsufficientBalance)."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"404":{"description":"No open position for the account in this market (NoOpenPosition).\n\nTwo causes, and one of them is transient. The position may genuinely not exist — or it may have been opened so recently that the risk store, which validates this route and folds committed fills asynchronously, has not seen the fill yet. A `NoOpenPosition` immediately after the order that opened the position is the second case; retry."},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"The adjustment was decided but could not be durably recorded, and was rolled back — collateral and allocated margin are unchanged (IsolatedMarginNotRecorded). Retry."}}}},"/leverage":{"post":{"operationId":"setLeverage","tags":["Account"],"summary":"Set per-market leverage for the connected account","description":"Stores the account's leverage for one market. It is a standing setting, not an order parameter: initial margin for every subsequent order in that market is `notional / leverage`, so raising it lowers the margin an order requires and lowering it raises it.\n\nAccepts any whole number in `1..=max`, where `max` is the market's effective ceiling — `min(max_leverage, floor(1 / initial_margin_rate))`, because the market's margin rate floors margin and so also caps leverage. Read `max_leverage` from `GET /markets/{market_id}/risk-params`. There is no fixed tier set; the preset chips a frontend offers (3x / 5x / 10x / 20x) are its own choice on top of this range.\n\nLeverage left unset behaves as the market maximum, so a client need not call this before its first order. It changes the initial margin a NEW order requires; it does not move the projected liquidation price, which `POST /orders/preview` reports identically at any leverage because that price is set by the maintenance-margin rate.","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"example":{"market_id":"BTC-USDX-PERP","leverage":10}}}},"responses":{"200":{"description":"The stored setting, echoed back.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LeverageResponse"},"example":{"market_id":"BTC-USDX-PERP","leverage":10}}}},"400":{"description":"`InvalidLeverage` — below the 1x floor (including `0`); or `LeverageExceedsMax` — above the market's effective ceiling, whose message carries both the requested value and that ceiling."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"404":{"description":"`MarketNotFound` — no such market."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-method":"setLeverage"}},"/account/credit":{"post":{"operationId":"credit","x-nexus-network-availability":["testnet","local"],"tags":["Account"],"summary":"Claim synthetic USDX credit","description":"Credit synthetic USDX to the authenticated account, up to a per-API-key daily allowance (default 500 USDX, resets at midnight UTC). `amount` is a decimal string; omit it to claim the full remaining daily allowance. Returns 429 with code `daily_limit_exceeded` once the allowance is used up, and 403 with code `credits_frozen` while crediting is administratively frozen. This is a **testnet-only** faucet: the credited USDX is synthetic and carries no real-world value. Mainnet has no synthetic credit — real-funds collateral arrives through the USDX bridge — so do not build a funding flow that assumes this operation exists on every network.","security":[{"hmacAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditRequest"},"example":{"amount":"500"}}}},"responses":{"200":{"description":"Credit applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditResponse"},"example":{"amount":"500","credited_today":"500","daily_limit":"500"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Refused. Either crediting is administratively frozen (`credits_frozen`), or a jurisdiction control refused the write (`US_RESTRICTED` / `GEO_UNRESOLVED` / `RESTRICTED_JURISDICTION`) — the latter are permanent for the caller's origin and must not be retried. Branch on `code`, which for the jurisdiction reasons equals the `x-nexus-block-reason` header. See “Jurisdiction restrictions” in the API description.","headers":{"x-nexus-block-reason":{"$ref":"#/components/headers/XNexusBlockReason"}},"content":{"application/json":{"examples":{"creditsFrozen":{"summary":"Crediting administratively frozen","value":{"code":"credits_frozen","message":"USDX crediting is temporarily frozen by an administrator. Existing balances remain tradeable."}},"usRestricted":{"summary":"US write restriction","value":{"code":"US_RESTRICTED","message":"This action is not available in the United States or to U.S. persons"}},"geoUnresolved":{"summary":"Origin could not be resolved; the write failed closed","value":{"code":"GEO_UNRESOLVED","message":"Unable to verify request origin; this action is unavailable"}}}}}},"429":{"description":"Daily credit allowance exhausted (resets at midnight UTC)","content":{"application/json":{"example":{"code":"daily_limit_exceeded","message":"daily USDX credit allowance reached for this API key","credited_today":"500","daily_limit":"500"}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Testnet-only balance credit and the internal deposit callback. Not client-facing in CCXT terms."}},"/account/deposit-target":{"get":{"operationId":"fetchDepositTarget","tags":["Account"],"summary":"How to fund this account","description":"Machine-readable funding instructions for the authenticated account, so an autonomous client can fund itself without out-of-band knowledge of how a given deployment accepts collateral.\n\n**The response is a discriminated union on `mode`, and which mode you get is a property of the deployment, not of the request.** Branch on `mode`; do not assume either shape. A deployment with a real deposit-contract address configured answers `onchain`; otherwise it answers `testnet-faucet` and points at its synthetic-credit endpoints. There is no request parameter that selects between them.\n\nAn address is never fabricated to fill the `onchain` shape. A deployment configured for on-chain deposits whose address is malformed refuses with `503` rather than silently falling back to the faucet, because depositing to a bad address burns the funds.\n\n`confirm` is identical in both modes and is the portable primitive: poll `GET /account` until `balance` reflects the funds.\n\nThis operation is **discovery only**. It moves no funds, creates no deposit, and has no side effect — acting on the returned `faucet` or `onchain` instruction is a separate, explicit step by the caller.","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Funding instructions for the authenticated account. Exactly one `mode` is returned; the fields beyond the common ones depend on it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepositTarget"},"examples":{"testnetFaucet":{"summary":"Deployment with no on-chain deposit contract configured","value":{"mode":"testnet-faucet","account":"0x742d35cc6634c0532925a3b844bc9e7595f0beb0","asset":"USDX","min_amount":"10","faucet":{"primary":{"method":"POST","path":"/account/credit","body":{"amount":"10"},"note":"Synthetic test USDX credited off-chain to your exchange balance. Per-API-key allowance (default 500 USDX/day). Omit `amount` to claim the remaining daily allowance."},"alternate":{"method":"POST","path":"/faucet","note":"Fixed per-wallet test USDX grant (default 10000 USDX), once per 24h per wallet."},"disclaimer":"Synthetic test USDX only — no on-chain transfer occurs. The on-chain deposit contract is not yet wired in; this endpoint returns mode=\"onchain\" with a real address once it ships."},"confirm":{"method":"GET","path":"/account","poll_field":"balance","note":"Credit is synchronous (well under the SPEC 30s credit target); poll /account until `balance` reflects the credit before trading."}}},"onchain":{"summary":"Deployment with an on-chain deposit contract configured","value":{"mode":"onchain","account":"0x742d35cc6634c0532925a3b844bc9e7595f0beb0","asset":"USDX","min_amount":"10","onchain":{"chain":"nexus-mainnet","asset":"USDX","address":"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984","token_address":"0xf92b58d2225a73b45ded3bc2290ac1a2077c1cf2","min_amount":"10","instructions":"Approve USDX spend for the deposit contract, then call depositTo(token, amount, beneficiary) with token set to `onchain.token_address`, amount in USDX base units, and beneficiary set to `account`. Only USDX is accepted; other tokens are rejected on-chain. Funds credit to the exchange account within 30s of on-chain confirmation."},"confirm":{"method":"GET","path":"/account","poll_field":"balance","note":"Poll until balance reflects the deposit (SPEC target: within 30s of on-chain confirmation)."}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Refused with `EARLY_ACCESS_REQUIRED`: this deployment restricts funding to early-access participants and the authenticated account is not one. Permanent for that account until it is enrolled — do not retry. The gate deliberately mirrors `POST /account/credit` and `POST /faucet`, so an account that cannot be funded is not told how to fund. Deployments with early access disabled never return it. The contract-wide `RESTRICTED_JURISDICTION` refusal can also surface as a `403` on any operation — see “Jurisdiction restrictions”.","content":{"application/json":{"example":{"code":"EARLY_ACCESS_REQUIRED","message":"Trading is currently restricted to early access participants. Connect the wallet you registered with on the Nexus testnet, or contact support to update your address."}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"`DEPOSIT_TARGET_MISCONFIGURED`: the deployment is configured for on-chain deposits but its deposit-contract address is not a well-formed address, so no funding target can be published. This is an operator misconfiguration rather than a transient fault; retrying does not clear it, and the endpoint deliberately refuses rather than answering `testnet-faucet`, which would mask the bad configuration.","content":{"application/json":{"example":{"code":"DEPOSIT_TARGET_MISCONFIGURED","message":"on-chain deposit address is misconfigured"}}}}},"x-ccxt-method":"fetchDepositAddress"}},"/account/rate-limit":{"get":{"tags":["Account"],"summary":"Get rate limit status","description":"Returns the authenticated caller's rate limit tier and, per budget, the ceiling, tokens remaining and reset timestamp — the same data surface as the X-RateLimit-* response headers, exposed as a queryable resource. This endpoint does not consume a rate limit token, so it can be polled freely to self-manage pacing without depleting the caller's budget. The top-level `limit`, `remaining` and `reset_at_ms` report the **request** class, which for an HMAC key is the binding minimum of the key and owner buckets; for unlimited-tier callers (gateway keys) they are null. `buckets` additionally reports each budget on its own — including `order` and `cancel`, the pools order writes and cancellations are actually charged to — keyed by the same labels a `429` uses, so a client can read its order-placement headroom rather than inferring it from the read budget. WebSocket ceilings are not reported here. See “Rate limits” in the API description.","operationId":"fetchRateLimitStatus","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Current rate-limit status for the caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitStatus"},"example":{"tier":"pro","limit":20,"remaining":17,"reset_at_ms":1765432100123,"buckets":{"key":{"limit":20,"remaining":17,"reset_at_ms":1765432100123},"owner":{"limit":20,"remaining":19,"reset_at_ms":1765432100051},"order":{"limit":20,"remaining":20,"reset_at_ms":0},"cancel":{"limit":20,"remaining":20,"reset_at_ms":0}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Rate-limit introspection. CCXT models rate limits client-side from `describe()`, not by asking the venue."}},"/account/cancel-on-disconnect":{"get":{"tags":["Account"],"summary":"Get cancel-on-disconnect status","description":"Returns the authenticated account's cancel-on-disconnect (COD) status. COD is an opt-in, per-account dead man's switch: when the account's last authenticated `/ws` connection drops and does not reconnect within the grace window, the exchange automatically cancels all of the account's resting orders, so a crashed client cannot leave orders exposed. `enabled` is the account's own opt-in; `active` additionally requires the exchange-side feature switch, so it reflects whether COD will actually fire; `grace_secs` is the exchange-configured reconnect window in seconds (null when the feature is unavailable). Clients that trade purely over REST and never open a `/ws` connection are not covered.","operationId":"fetchCancelOnDisconnect","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Current cancel-on-disconnect status for the account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelOnDisconnectStatus"},"example":{"enabled":true,"active":true,"grace_secs":10}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Cancel-on-disconnect is a Nexus session policy, not a CCXT method."},"put":{"tags":["Account"],"summary":"Set cancel-on-disconnect","description":"Enables or disables cancel-on-disconnect for the authenticated account. Opt-in is per account and off by default: someone who deliberately leaves a passive resting order while offline should not have a brief blip cancel it. Enable it when you want the guarantee that a dead client cannot keep orders resting — typical for market makers and algorithmic traders. Returns the resulting COD status (same shape as the GET).","operationId":"setCancelOnDisconnect","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetCancelOnDisconnectRequest"},"example":{"enabled":true}}}},"responses":{"200":{"description":"The resulting cancel-on-disconnect status for the account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelOnDisconnectStatus"},"example":{"enabled":true,"active":true,"grace_secs":10}}}},"400":{"description":"Malformed request body."},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Cancel-on-disconnect is a Nexus session policy, not a CCXT method."}},"/positions":{"get":{"tags":["Positions"],"summary":"List open positions","operationId":"fetchPositions","x-ccxt-method":"fetchPositions","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"The account's open positions.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Position"}},"example":[{"market_id":"BTC-USDX-PERP","side":"Long","size":"0.5","entry_price":"49500.00","unrealized_pnl":"250.50","realized_pnl":"0.00","liquidation_price":null,"liquidation_price_error":"margin_state_not_mirrored","notional_value":"25000.50","notional_value_error":null,"roe":"0.2004","roe_error":null,"margin_used":"1250.03","margin_used_error":null,"max_leverage":20,"max_leverage_error":null,"funding_paid":"12.50","leverage":null,"leverage_error":"margin_state_not_mirrored"}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/fills":{"get":{"operationId":"fetchFills","tags":["Account"],"summary":"List your fills","description":"Returns up to 1000 fills for the authenticated account, newest first. Fills are trade executions resulting from order matches — each fill carries the matched price, quantity, fee, and whether it was the taker or maker side.","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":1000},"description":"Fills per page (default 100, capped at the 1,000-fill retained window). Bounds one page, not a cursor walk — see “Cursor pagination”."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Array of fills, newest first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Fill"}},"example":[{"id":"cf72c7f3-1234-5678-abcd-ef0123456789","order_id":"ord_a1b2c3d4e5f6...","market_id":"BTC-USDX-PERP","side":"buy","price":"84250.00","size":"0.01","fee":"0.84","taker_or_maker":"taker","timestamp":1779225381434,"is_liquidation":false}]}},"headers":{"X-Next-Cursor":{"$ref":"#/components/headers/XNextCursor"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-weight":5,"x-ccxt-method":"fetchMyTrades"}},"/withdrawals":{"post":{"operationId":"createWithdrawal","tags":["Account"],"summary":"Submit a withdrawal","description":"Withdraws USDX collateral from the venue back to the signing wallet.\n\n**The signature is the authentication, not an API key.** This operation takes no `X-API-Key` and no session token: the caller signs an EIP-712 `WithdrawIntent` with the wallet's own key and the server recovers the signer from `signature`. The recovered address must equal `wallet`, or the request is refused with `401 SIGNER_MISMATCH`. `security` is omitted rather than `[]` for that reason — the operation is not public, it is authorized by something this document has no scheme for, the same way `POST /agents/register` expresses it.\n\nEIP-712 domain: `{ name: 'Nexus Exchange', version: '1', chainId: 20056 }`. Unlike `POST /agents/register`, the chain id is **not** a request field here: the server always verifies against `20056`, so a signature produced under any other domain simply does not recover `wallet` and is refused with `401 SIGNATURE_INVALID`. Sign with that value; do not substitute a network chain id.\nTyped data type: `WithdrawIntent { uint256 amount, address asset, uint64 nonce }`, where `asset` is the USDX sentinel address `0xcccccccccccccccccccccccccccccccccccccccc`. The venue is single-collateral and ignores the asset; the address exists only so the typed data has a stable binding.\n\n**The destination is implicit.** Funds are credited to the signing wallet; the typed data carries no destination, so there is nothing to redirect. `destination` may be sent for clarity but must equal `wallet` — any other value is refused with `400 destination_locked`.\n\n**`nonce` is single-use per wallet.** Re-submitting an already-accepted `(wallet, nonce)` pair is refused with `401 NONCE_REPLAY` and creates no second withdrawal, so a client that retries on an ambiguous network error cannot double-withdraw. This operation is not idempotent: a replay is rejected rather than answered with the original result.\n\n**`amount` is a whole smallest-unit count.** It is a decimal string on the wire, but `WithdrawIntent` commits to a `uint256`, so a fractional value cannot be represented and is refused with `400 fractional_amount` rather than rounded.\n\n**Exposure must be closed first.** A withdrawal that would leave an open position under-collateralized is refused with `400 WITHDRAW_BLOCKED_BY_OPEN_POSITION`; close positions and cancel resting orders before withdrawing.\n\n**Acceptance is not settlement.** A `200` means the collateral was debited and the withdrawal was recorded with status `pending`; it does not mean funds have moved on-chain. Poll `GET /withdrawals` for the lifecycle `status` and for `tx_hash`, which stays `null` until the withdrawal is submitted on-chain.","x-nexus-auth":"wallet-signature","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawalRequest"},"example":{"wallet":"0xAbCdEf0123456789AbCdEf0123456789AbCdEf01","amount":"500","asset":"USDX","nonce":1779225381434,"signature":"0xdeadbeef..."}}}},"responses":{"200":{"description":"Withdrawal accepted and collateral debited. The withdrawal is recorded with status `pending`; this is an acceptance, not an on-chain settlement.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawalResponse"},"example":{"balance":"9500.00","id":42,"amount":"500.00","status":"pending","timestamp":1779225381434}}}},"400":{"description":"The withdrawal was refused before or by the engine. Branch on `code`:\n- `WITHDRAW_BLOCKED_BY_OPEN_POSITION` — the account has open exposure; close it first.\n- `INSUFFICIENT_BALANCE` — the requested amount exceeds withdrawable collateral.\n- `INVALID_AMOUNT` — `amount` is absent, non-numeric, or not greater than zero.\n- `fractional_amount` — `amount` has a fractional part; `WithdrawIntent` carries a `uint256` smallest-unit count.\n- `destination_locked` — `destination` was supplied and does not equal `wallet`.\n- `BAD_WALLET` — `wallet` is not a 0x-prefixed 20-byte address.\n- `INVALID_JSON` / `BAD_REQUEST` — the body could not be read or parsed.\n\nAn engine rejection with no dedicated classification is forwarded unchanged; it carries the same value under both `error` and `code`.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawalError"},"examples":{"open_position":{"summary":"Open exposure blocks the withdrawal","value":{"code":"WITHDRAW_BLOCKED_BY_OPEN_POSITION","message":"close open positions before withdrawing"}},"insufficient_balance":{"summary":"Amount exceeds withdrawable collateral","value":{"code":"INSUFFICIENT_BALANCE","message":"Required: 500, available: 100"}},"fractional_amount":{"summary":"Amount is not a whole smallest-unit count","value":{"code":"fractional_amount","message":"WithdrawIntent amount must be an integer (smallest-unit count)"}},"destination_locked":{"summary":"Explicit destination does not equal the signing wallet","value":{"code":"destination_locked","message":"WithdrawIntent destination is implicitly the signing wallet (D-7); explicit destination must equal wallet or be omitted"}},"forwarded_engine_rejection":{"summary":"Unclassified engine rejection, forwarded unchanged","value":{"error":"InvalidAmount","code":"InvalidAmount","message":"Amount must be a positive number"}}}}}},"401":{"description":"The signature did not authorize this withdrawal. Branch on `code`:\n- `NONCE_REPLAY` — this `(wallet, nonce)` pair was already accepted. No second withdrawal was created; sign a fresh nonce.\n- `SIGNER_MISMATCH` — the address recovered from `signature` is not `wallet`. The response echoes the `claimed` and `recovered` addresses.\n- `SIGNATURE_INVALID` — `signature` is malformed, not 65 bytes, non-canonical (high-S), or recovered no key.\n- `STORE_ERROR` — the nonce could not be recorded, so the withdrawal was not attempted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawalError"},"examples":{"nonce_replay":{"summary":"Replayed signed intent","value":{"code":"NONCE_REPLAY","message":"nonce already used"}},"signer_mismatch":{"summary":"Signature does not recover to the claimed wallet","value":{"code":"SIGNER_MISMATCH","message":"Signature does not match wallet","claimed":"0xAbCdEf0123456789AbCdEf0123456789AbCdEf01","recovered":"0x1234567890AbCdEf1234567890AbCdEf12345678"}}}}}},"403":{"description":"Withdrawals are refused for this wallet or this deployment. All three cases are decided before any signature work, so no nonce is consumed and a retry with the same signed intent is safe once the condition clears. Branch on `code`, or on `error` for the freeze envelope:\n- `WITHDRAWALS_DISABLED` (`code`) — the deployment-wide withdrawal kill switch is off.\n- `withdrawals_frozen` (`error`) — the exchange is frozen; balances remain tradeable but new outflows are suspended.\n- `EARLY_ACCESS_REQUIRED` (`code`) — the wallet is not on the early-access allowlist on a deployment that enforces one.\n\nJurisdiction controls do **not** appear here: withdrawal paths are exempt from them by design, because a restriction that traps funds is worse than no restriction.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WithdrawalError"},"examples":{"withdrawals_disabled":{"summary":"Deployment-wide kill switch","value":{"code":"WITHDRAWALS_DISABLED","message":"Withdrawals are currently disabled on this venue."}},"withdrawals_frozen":{"summary":"Exchange frozen; new outflows suspended","value":{"error":"withdrawals_frozen","message":"Withdrawals are suspended while the exchange is frozen."}}}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-method":"withdraw"},"get":{"operationId":"fetchWithdrawals","tags":["Account"],"summary":"List your withdrawals","description":"Returns up to 100 withdrawal records for the authenticated account, newest first.","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":100},"description":"Maximum records to return"}],"responses":{"200":{"description":"Array of withdrawal records","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundsEntry"}},"example":[{"id":42,"kind":"withdrawal","address":"0x1111111111111111111111111111111111111111","amount":"500.00","currency":"USDX","timestamp":1779225381434,"status":"confirmed","txid":"0xabababababababababababababababababababababababababababababababab","updated":1779225391434}]}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-method":"fetchWithdrawals"}},"/ws":{"get":{"operationId":"connectWebSocket","tags":["WebSocket"],"summary":"Per-account WebSocket stream","description":"WebSocket endpoint for both public market data and per-account private channels. Requires a token from POST /ws/token:\n```\nwss://host/ws?token=YOUR_TOKEN\n```\nTokens are single-use and expire after 60 seconds, and they are scoped to the network that minted them — mint the token on the same host you connect to.\n\nEvery client→server and server→client message is a JSON envelope tagged with an `op` field. Subscribe to one channel per message:\n```json\n{\"op\": \"subscribe\", \"channel\": \"trades\", \"market\": \"BTC-USDX-PERP\"}\n{\"op\": \"subscribe\", \"channel\": \"fills\"}\n```\nUnsubscribe with `{\"op\": \"unsubscribe\", \"channel\": \"...\", \"market\": \"...\"}`.\n\n**Public channels** (the token is always required to upgrade): `trades`, `book`, `candles`, `ticker` — each requires a `market` field.\n\n`ticker` carries the same payload `GET /markets/{market_id}/ticker` returns — both are built by one function, so the REST and streaming views of a ticker cannot disagree. It is published once per book-poll cycle (currently 1 s) rather than per trade, because the 24h high, low, open and volume come from the materialized candles and a running notional, not from any single fill; a ticker projected off trades would move `last` and leave the 24h fields stale. Frames are emitted every cycle whether or not the values moved, as `book` is — diff against the previous frame if you only care about changes. `engine` is public and venue-wide, so it takes no `market` field: subscribe and unsubscribe are accepted and acked, but no frames are published on it yet. Treat it as reserved — do not build against a payload shape.\n\n**Per-account channels** (scoped to the wallet that minted the token): `orders`, `fills`, `positions`, `balances`, `liquidations`.\n\n**Server messages:**\n- `{\"op\": \"subscribed\", \"channel\": \"fills\", \"market\": null, \"seq_at_join\": 42}` — subscribe ack; `seq_at_join` is the channel's current sequence at attach time, used as the reconnect cursor.\n- `{\"op\": \"unsubscribed\", \"channel\": \"fills\", \"market\": null}` — unsubscribe ack.\n- `{\"op\": \"event\", \"channel\": \"fills\", \"market\": null, \"seq\": 43, \"payload\": {...}}` — a delivered event; `seq` is monotonic per channel.\n- `{\"op\": \"out_of_sync\", \"channel\": \"fills\", \"market\": null, \"oldest_seq\": 100}` — the requested `since` cursor fell behind the replay buffer; refetch state over REST and resubscribe with a fresh cursor.\n- `{\"op\": \"error\", \"message\": \"...\"}` — invalid op, unknown channel, or bad message format.\n\n**`liquidations` payloads:** the `payload` of an `event` on this channel is externally tagged — exactly one key, naming the engine event variant. See the `LiquidationEvent` schema for the full shape. Monetary fields are lossless decimal strings, never JSON numbers.\n- `LiquidationAlert` — a pre-liquidation risk warning for the account. `severity` is `Warning`, `Critical` or `Imminent`, classified from `equity / maintenance_margin`: `Warning` in (1.2, 1.5], `Critical` in (1.05, 1.2], `Imminent` in (1.0, 1.05]. Nothing is emitted above 1.5 (safe) or at/below 1.0 (already liquidating). `market_id` is `null` for a portfolio-level alert over the whole cross-margin account — the only form the engine emits today — and names a market when the alert is scoped to one market.\n- `PortfolioLiquidation` — terminal: the account's cross-margin positions have already been closed out. `closures` carries the per-market closes.\n\nAlerts are **edge-triggered**: one frame per worsening severity transition, nothing while a severity holds, and nothing when the account recovers. Subscribing does not replay the current alert state — unlike `balances` and `positions`, this channel is not seeded with a snapshot on subscribe — and there is no REST read of it, so a reconnecting client sees nothing until the next worsening transition.\n```json\n{\"op\": \"event\", \"channel\": \"liquidations\", \"market\": null, \"seq\": 7, \"payload\": {\"LiquidationAlert\": {\"account_id\": \"0x1111111111111111111111111111111111111111\", \"market_id\": null, \"severity\": \"Critical\", \"equity\": \"1150.00\", \"maintenance_margin\": \"1000.00\", \"sequence\": 9412, \"epoch\": 3, \"emitted_at\": 1750000000000}}}\n```\n\n**Reconnection:** tokens are single-use — mint a new one via POST /ws/token before reconnecting. To resume a channel without gaps, pass the last `seq` you received:\n```json\n{\"op\": \"subscribe\", \"channel\": \"fills\", \"since\": 42}\n```\n\n**Control-plane limits.** The WebSocket ceilings are a resource class of their own: they are independent of the REST request budget and of the trading-action budget, and exhausting one does not affect the others. Per tier — `Pro`: 5 concurrent connections, 50 subscriptions, 10 inbound client frames per second; `MarketMaker`: 100, 1,000 and 50/s; gateway (`Unlimited`) keys are exempt from all three. Those connection figures are **per account**: a separate per-IP cap (5 by default) is applied first, at upgrade time, and binds on every tier including `Unlimited`, so one origin address cannot reach the per-account figure on its own. A connection refused there gets an HTTP `429` (`ws_conn_limit_exceeded`) at the upgrade rather than a close frame. A `2×` burst is tolerated above the sustained frame rate. The subscription ceiling is applied both per connection and across all of an account's connections at the same number, so opening more sockets does not buy more subscriptions; re-subscribing a key you already hold replaces it in place and is free. Exceeding it returns an `error` frame (`subscription_limit_exceeded`) rather than a `429` — there is no status code on this surface — and sustained inbound flooding closes the connection with `1008`. These figures are current per-deployment configuration, not part of this contract. See “Rate limits” in the API description.","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"Short-lived token from POST /ws/token"}],"responses":{"101":{"description":"Switching Protocols — WebSocket upgrade."}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"WebSocket transport and token minting. Covered by the CCXT Pro channel map, not by a REST unified method."}},"/stream":{"get":{"operationId":"connectStream","tags":["WebSocket"],"summary":"Public WebSocket stream (legacy)","description":"Legacy public-only WebSocket endpoint. Prefer GET /ws which also supports per-account channels. **No token is required.** Token auth was removed from this endpoint in ENG-3128 — the token store was per-instance, so a token minted on one instance was unknown to another during a rolling deploy and the upgrade failed with a spurious 401. The upgrade is served unauthenticated on purpose: this channel carries only public market data (trades, book, stats) and never per-account data. `security` being absent above is therefore accurate, and this sentence used to contradict it. Unlike /ws, the protocol is a single untagged subscribe message listing channels:\n```json\n{\"subscribe\": [\"trades:*\", \"book:BTC-USDX-PERP\", \"stats\"]}\n```\nChannels: `trades:*` / `trades:{market_id}`, `book:*` / `book:{market_id}`, `stats`.","parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string"},"description":"Short-lived token from POST /ws-tokens"}],"responses":{"101":{"description":"Switching Protocols — WebSocket upgrade."}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"WebSocket transport and token minting. Covered by the CCXT Pro channel map, not by a REST unified method."}},"/admin/tiers":{"put":{"operationId":"setTier","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Operator-only surface. CCXT models a trading client's view of a venue; it has no concept of an exchange's own administration, so there is no unified method to map to.","tags":["Admin"],"summary":"Set account tier","description":"Assign a rate-limit tier to an address. Invalidates the existing rate-limit bucket so the new cap takes effect immediately.","security":[{"adminAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"example":{"address":"0x1234...abcd","tier":"MarketMaker"}}}},"responses":{"200":{"description":"The updated account tier.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TierEntry"},"example":{"address":"0x1234...abcd","tier":"MarketMaker"}}}},"403":{"description":"Admin secret required"}}},"get":{"operationId":"listTiers","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Operator-only surface. CCXT models a trading client's view of a venue; it has no concept of an exchange's own administration, so there is no unified method to map to.","tags":["Admin"],"summary":"List tier overrides","description":"Returns all addresses with non-default tier assignments.","security":[{"adminAuth":[]}],"responses":{"200":{"description":"The configured tier overrides.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TierEntry"}},"example":[{"address":"0x1234...abcd","tier":"MarketMaker"}]}}},"403":{"description":"Admin secret required"}}}},"/admin/tiers/{address}":{"delete":{"operationId":"deleteTier","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Operator-only surface. CCXT models a trading client's view of a venue; it has no concept of an exchange's own administration, so there is no unified method to map to.","tags":["Admin"],"summary":"Reset account tier","description":"Remove a tier override, reverting the address to the default Pro tier.","security":[{"adminAuth":[]}],"parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string"},"description":"Ethereum address (0x-prefixed)"}],"responses":{"200":{"description":"Tier removed"},"403":{"description":"Admin secret required"},"404":{"description":"Address not in allowlist"}}}},"/stats":{"get":{"tags":["Markets"],"summary":"Venue statistics","description":"Aggregate venue statistics plus rolling unique-trader counts. Public — no authentication required.","operationId":"fetchStats","security":[],"responses":{"200":{"description":"Venue statistics snapshot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsSnapshot"}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Venue-wide analytics. CCXT models per-market data, not venue aggregates."}},"/stats/history":{"get":{"tags":["Markets"],"summary":"Venue throughput history","description":"Per-second throughput ring buffer (up to 3600 points). Public — no authentication required.","operationId":"fetchStatsHistory","security":[],"responses":{"200":{"description":"Throughput samples, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ThroughputSample"}}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Venue-wide analytics. CCXT models per-market data, not venue aggregates."}},"/stats/balance-distribution":{"get":{"operationId":"fetchBalanceDistribution","x-nexus-network-availability":["testnet","local"],"tags":["Statistics"],"summary":"Aggregate balance distribution","description":"Account counts per collateral band, plus the total number of accounts the indexer holds a balance projection for.\n\n**Aggregate only.** Bucket counts and a total — never a per-account balance, address or id. Reserved system accounts are excluded, so the counts are users rather than venue inventory.\n\n`network` names the chain network this indexer serves and `testnet` is the same fact as a boolean, both derived from the instance's required `auth.network` configuration rather than asserted (ENG-12796). `note` is swapped by network rather than qualified: on a synthetic-funds network it states that balances are faucet-funded, that the projection is in-memory and resets on redeploy, and that the figures are not real funds; on `mainnet` it states the aggregation and the redeploy reset without those clauses. Never present a synthetic-funds figure as real funds.","security":[],"responses":{"200":{"description":"Current aggregate distribution","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BalanceDistribution"},"example":{"buckets":[{"min":"0","max":"100","count":412},{"min":"100","max":"1000","count":188},{"min":"1000","count":27}],"account_count":627,"network":"testnet","testnet":true,"note":"Illustrative testnet data. Balances are faucet-funded and ephemeral (in-memory projection, reset on redeploy); these figures do not represent real funds."}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Venue-wide analytics. CCXT models per-market data, not venue aggregates."}},"/stats/volume":{"get":{"operationId":"fetchCumulativeVolume","x-nexus-network-availability":["testnet","local"],"tags":["Statistics"],"summary":"Cumulative traded volume","description":"Cumulative traded notional in quote (USDX): the sum of `price * quantity` over deduplicated `Fill` events, counted once per fill (a fill carries both sides of the trade, so it is not doubled), liquidation prints included. Delisted markets keep contributing — a cumulative total must not fall when a market leaves the listing.\n\nReturned as decimal STRINGS, not JSON numbers. The same accumulator reaches `GET /markets/summary` and `GET /tickers` through a lossy `f64` conversion; this route does not, so the two surfaces can disagree in the last digits and this one is the exact figure.\n\n**This is not a 24h window.** `MarketSummary.volume_24h` and `Ticker.quoteVolume` are the SAME accumulator under a misleading name: nothing decays either of them. Read `GET /stats/volume` when you want the cumulative figure stated honestly, with the coverage start that says what it is cumulative since.\n\n**`coverage_start_ms` is part of the contract, not a nicety.** The figure is NOT cumulative since inception. This is an in-memory projection: whenever it is rebuilt, the total restarts from zero and `coverage_start_ms` moves forward with it. So compare the two across scrapes — a `coverage_start_ms` later than the one you saw last is what distinguishes a reset from a quiet venue, and a consumer that publishes the total alone is publishing a number that can silently drop. `network` names the chain network this indexer serves and `testnet` is the same fact as a boolean, both derived from `auth.network` rather than asserted (ENG-12796); `note` restates this in prose.","security":[],"responses":{"200":{"description":"Cumulative traded notional, venue total and per market","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CumulativeVolume"},"example":{"cumulative_volume_quote":"48210355.42","coverage_start_ms":1785542400000,"markets":[{"market_id":"BTC-USDX-PERP","cumulative_volume_quote":"31540220.10","coverage_start_ms":1785542400000},{"market_id":"ETH-USDX-PERP","cumulative_volume_quote":"16670135.32","coverage_start_ms":1785888000000}],"network":"testnet","testnet":true,"note":"Illustrative testnet data. Cumulative traded notional in quote (USDX) since `coverage_start_ms`: the sum of price x quantity over deduplicated fills, counted once per fill, liquidation prints included. NOT a 24h window, and NOT cumulative since inception. This is an in-memory projection: whenever it is rebuilt, the total restarts from zero and `coverage_start_ms` moves forward with it. Publish and compare the two together — a `coverage_start_ms` later than the one you saw last is what tells you the total dropped rather than the venue going quiet."}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Venue-wide analytics. CCXT models per-market data, not venue aggregates."}},"/stats/open-interest":{"get":{"operationId":"fetchOpenInterest","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Venue-wide as specified, and ccxt's unified method is per-symbol. `fetchOpenInterest(symbol)` returns one market's open interest; this operation takes no parameters and returns every market plus a venue total, and ccxt 4.5.76 has no plural form (`ex.has['fetchOpenInterests']` is None, checked). Annotating it `fetchOpenInterest` would put a CCXT badge over an operation an adapter cannot call that way — the concept matches, the contract does not. Revisit if a per-market variant is added here, or if ccxt gains a plural method; the operationId already anticipates the mapping.","x-nexus-network-availability":["testnet","local"],"tags":["Statistics"],"summary":"Open interest, long and short reported separately","description":"Open interest per market and as a venue total, with the **long and short sides reported separately** rather than pre-summed.\n\n**Read this first: which number to publish.** On a perpetual venue every long position is matched by an equal short, so `long_oi == short_oi` holds by construction and their sum is exactly TWICE the one-sided figure reference venues such as Hyperliquid and dYdX headline. Caption `long_oi_quote` (or `short_oi_quote`, which equals it). `gross_oi_two_sided_quote` is the sum, named so it cannot be mistaken for the one-sided figure — **it is NOT the quantity the BFF's `total_open_interest` on `GET /market-stats` carries** — that figure sums `/admin/risk-summary`'s per-market totals, which are raw position sizes in BASE units with the mark never multiplied in. To reconcile per market, compare the BFF's `open_interest` against `long_oi_base + short_oi_base`. There is deliberately no venue-level counterpart, because base units do not add across markets.\n\n**Base versus quote.** `*_base` is the size open in the market's BASE asset (BTC for `BTC-USDX-PERP`). Base units do NOT add across markets — 3 BTC and 50 ETH sum to nothing — so the venue-level figures are quote-only and `*_base` appears per market only. `*_quote` is that size priced into quote notional (USDX) at `mark_price`.\n\n**Where the mark price comes from, and when a quote figure is absent.** `mark_price_source` names it on every response. The mark is this indexer's mirror of the engine's `GET /markets/{market_id}/mark-price`, refreshed roughly every second and **withheld once it is older than 15 seconds** rather than served frozen. A market whose mark is absent, stale, or whose notional overflows keeps its `*_base` figures and carries `quote_error` instead of `*_quote`; absence is never zero. If a market that CARRIES open interest could not be priced, the venue `*_quote` fields are omitted too and the top-level `quote_error` names the markets — a partial venue total is never presented as complete. The venue figures are also withheld when the venue fold itself overflows `Decimal`, and that message names no market because every row priced; read `quote_error` rather than inferring the cause. A market with NOTHING open is exempt from all of that: no mark can change a zero, so it prices to `\"0\"` while still carrying the `quote_error` that says why it has no mark, and it never withholds the venue figure. Withholding the headline number because an EMPTY market lost its mark would lose it for the one reason that provably cannot affect it.\n\n**Freshness, and the one thing no error field can tell you.** This is an in-memory projection folded from the engine's position stream and repaired from the engine's authoritative snapshot on a background sweep. Shortly after the service is rebuilt the mirror can be partially hydrated, and the figure reads low; the fold cannot distinguish that from a genuinely quiet venue, so no error field reports it. The sharpest form is a venue figure of `\"0\"` in the first seconds after a restart, when listed markets already exist and no mark has landed yet: that is the correct answer for what the mirror holds and the wrong one for what the venue holds, and this response cannot tell you which you are reading. Do not alert on a drop to zero without checking `as_of_ms` and the per-market `quote_error` values. `as_of_ms` — the last event this indexer ingested, on its OWN clock — is what tells a fresh snapshot from a stalled fold. It is not the last time open interest changed.\n\nValues are decimal STRINGS, not JSON numbers, so they are exact; parse with a decimal type, never a float. Every listed market gets an entry, carrying `\"0\"` on both sides when nothing is open, so \"no open interest\" is distinguishable from \"market unknown\"; a delisted market appears only while it still carries a position. `network` names the chain network this indexer serves and `testnet` is the same fact as a boolean, both derived from `auth.network` rather than asserted (ENG-12796); `note` restates the one-sided warning in prose.","security":[],"responses":{"200":{"description":"Open interest, venue total and per market","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenInterest"},"example":{"as_of_ms":1787678930804,"long_oi_quote":"12550000.0","short_oi_quote":"12550000.0","gross_oi_two_sided_quote":"25100000.0","markets":[{"market_id":"BTC-USDX-PERP","long_oi_base":"125.5","short_oi_base":"125.5","long_oi_quote":"12550000.0","short_oi_quote":"12550000.0","gross_oi_two_sided_quote":"25100000.0","mark_price":"100000","mark_price_as_of_ms":1787678930100},{"market_id":"ETH-USDX-PERP","long_oi_base":"0","short_oi_base":"0","long_oi_quote":"0","short_oi_quote":"0","gross_oi_two_sided_quote":"0","mark_price":"3000","mark_price_as_of_ms":1787678930100},{"market_id":"SOL-USDX-PERP","long_oi_base":"0","short_oi_base":"0","long_oi_quote":"0","short_oi_quote":"0","gross_oi_two_sided_quote":"0","quote_error":"no mark price has been mirrored for this market yet"}],"mark_price_source":"Indexer mirror of the engine's GET /markets/{market_id}/mark-price, refreshed by book_poller on a ~1s cadence and withheld once older than 15000 ms (ENG-5909). Quote figures are base size x this mark, multiplied in exact decimal.","network":"testnet","testnet":true,"note":"Illustrative testnet data. Open interest is a stock, not a flow: each side is the size currently open, reported separately and never pre-summed. On a perpetual venue every long is matched by a short, so the two sides are equal by construction and `gross_oi_two_sided_quote` is exactly twice the one-sided figure that reference venues headline — caption a one-sided field, not the gross one. Base units are per market and do not add across markets, so the venue totals are quote-only. This is an in-memory projection rebuilt from the engine's position stream, so shortly after a rebuild it can be partially hydrated and read low; the fold cannot distinguish that from a quiet venue, so compare `as_of_ms` — the last event this indexer ingested, on its own clock — rather than assuming the figure is settled. `as_of_ms` is not the last time open interest changed. In particular a venue figure of `0` shortly after a restart is what an unhydrated mirror looks like as well as what a genuinely flat venue looks like, and nothing in this response can tell them apart; a per-market `quote_error` saying no mark has been mirrored yet is a strong hint you are reading the first case. Do not alert on a drop to zero without checking that."}}}}}}},"/status":{"get":{"tags":["Markets"],"summary":"Aggregate service health","description":"Aggregate health of indexer/engine/oracle/bots for status pages. Public — no authentication required.","operationId":"fetchStatus","security":[],"responses":{"200":{"description":"Service health summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServiceHealth"}}}}},"x-ccxt-method":"fetchStatus"}},"/metadata":{"get":{"tags":["Markets"],"summary":"API version metadata","description":"Programmatic API-version discovery. Unauthenticated and exempt from the version gate itself, so a client that has just been rejected with `426 api_version_unsupported` can always reach the value it needs to self-heal — read `api_version.min_supported`, regenerate against `spec_url`, and retry with the `X-Nexus-Api-Version` header (ENG-5365).\n\nStatic per deployment: the body is rendered once at boot from the instance's `[api_version]` configuration and served unchanged, so it reflects the running build rather than live state.\n\nVersions here are bare `MAJOR.MINOR.PATCH` with no `v` prefix, unlike the released spec tags. Root mount only — the edge does not serve an `/api/v1/metadata` sibling.\n\nThis is not the shape described by the unreferenced `Metadata` schema in this contract; see that schema's own description.","operationId":"fetchApiVersionMetadata","security":[],"responses":{"200":{"description":"The version window this deployment serves.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiVersionMetadata"},"example":{"api_version":{"header":"x-nexus-api-version","min_supported":"0.0.0","current":"0.9.57","deprecated_below":null,"sunset":null,"spec_url":"https://github.com/nexus-xyz/nexus-exchange-api/releases","docs_url":"https://docs.nexus.xyz/exchange/apis-and-rates/api-versioning","policy":"pre-1.0: minimum-supported version may advance until GA; missing version header allowed during grace mode"}}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Transport-level version negotiation for this venue's own API. CCXT has no method for it and handles client compatibility in its own adapter layer."}},"/markets/{market_id}/funding-samples":{"get":{"tags":["Funding"],"summary":"Funding premium-index samples","description":"Dense per-tick premium-index samples (60s cadence, up to 480 points = 8h). Public — no authentication required.","operationId":"fetchFundingSamples","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":480,"maximum":480},"description":"Maximum samples to return (capped at 480)."}],"responses":{"200":{"description":"Premium-index samples, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundingPremiumSample"}}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Premium-index samples between funding settlements, not funding rates. CCXT's only funding-rate structure is `FundingRateHistory` (a settled `fundingRate` at a timestamp), and `GET /markets/{market_id}/funding` already serves it as fetchFundingRateHistory. `FundingPremiumSample` carries a timestamp and `premium_index`, and states that the settled rate is a property of the settlement window rather than of an intra-window sample, so a fetchFundingRateHistory badge here would publish a `fundingRate` nothing can fill. fetchPremiumIndexOHLCV is the only other candidate in ccxt 4.5.76 and does not describe it either: it returns OHLCV candles, and these are scalar point observations. Reachable natively as the implicit publicGetMarketsMarketIdFundingSamples."}},"/account/fees":{"get":{"tags":["Account"],"summary":"Account fee schedule","description":"The authenticated account's effective fee schedule — maker/taker rate (bps), fee tier, rolling 30-day traded volume, and active discounts — for parity with Hyperliquid `userFees`. This is the forward-looking *schedule* rate, not a realized average: the venue charges a per-fill fee but does not emit a realized per-fill rate. The reported rate's scope is given by `schedule` (see the schema); the account id is taken from the authenticated credentials, not a parameter. Pass `market_id` to request the exact current mirrored schedule for one intended market before the account has traded there. A selected market never falls back to a venue-modal or unrelated market rate: an unavailable or stale target returns `schedule: unknown`, zero sentinels, and an empty `markets` array.","operationId":"fetchAccountFees","parameters":[{"name":"market_id","in":"query","required":false,"schema":{"type":"string","pattern":"^[A-Z0-9]+(-[A-Z0-9]+)*$","maxLength":64},"example":"BTC-USDX-PERP","description":"Optional exact target for an account fee preflight. When present and currently mirrored, the response contains exactly this market in `markets` and uses its maker/taker pair as the headline. A well-formed target whose fee parameters are unavailable or stale returns HTTP 200 with `schedule: unknown`, zero headline sentinels, and no rows; it never falls back to an unrelated venue reference. A malformed value, including supplying the scalar parameter more than once, is rejected with `400` (`INVALID_MARKET_ID`). The raw query string is part of the HMAC canonical request and must be signed exactly as sent."}],"security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Account fee schedule. With `market_id`, a known target is the sole `markets` row and supplies the headline pair; an unavailable or stale target fails closed as `unknown` rather than substituting another market's rate.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountFees"},"example":{"maker_fee_bps":-2,"taker_fee_bps":5,"tier":"base","schedule":"per_market","markets":[{"symbol":"BTC-USDX-PERP","maker_fee_bps":-2,"taker_fee_bps":5},{"symbol":"ETH-USDX-PERP","maker_fee_bps":-2,"taker_fee_bps":5}],"volume_30d":"101005.00","volume_30d_estimated":false,"discounts":[]}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-method":"fetchTradingFees"}},"/account/summary":{"get":{"tags":["Account"],"summary":"Account portfolio summary","operationId":"fetchAccountSummary","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Portfolio summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPortfolioSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/AuthoritativeMarginUnavailable"}},"x-nexus-rate-limit-weight":5,"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus account aggregates. `fetchBalance` is the CCXT-shaped view; these are richer and unmapped by design."}},"/account/state":{"get":{"tags":["Account"],"summary":"Consolidated account state","description":"Full account state in a single call: the portfolio summary aggregates plus all open positions (`{ summary, positions }`). Saves clients from pairing `/account/summary` with `/positions`, matching Hyperliquid `clearinghouseState` ergonomics. Both parts are built from one coherent read, so `summary.open_positions_count` always matches the `positions` length, and the embedded `summary` is identical to the standalone `/account/summary` response. Fails closed with `502` when the engine-authoritative margin view is unavailable.","operationId":"fetchAccountState","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Consolidated account state (summary aggregates + open positions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountState"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/AuthoritativeMarginUnavailable"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus account aggregates. `fetchBalance` is the CCXT-shaped view; these are richer and unmapped by design."}},"/account/equity-history":{"get":{"tags":["Account"],"summary":"Account equity history","description":"Equity time-series for the authenticated account (5s cadence, ~1h window), oldest first.","operationId":"fetchEquityHistory","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":720},"description":"Points per page (default 100, capped at the 720-point retained window). Points sharing one sample millisecond are returned as one run, so a page can exceed this value — see “Cursor pagination”."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Equity samples, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EquityPoint"}}}},"headers":{"X-Next-Cursor":{"$ref":"#/components/headers/XNextCursor"}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Portfolio and equity time series. CCXT has no unified historical-equity method."}},"/account/portfolio-history":{"get":{"tags":["Account"],"summary":"Account portfolio time-series","description":"Portfolio time-series for the authenticated account — equity, cumulative trading PnL, and cumulative traded volume — downsampled over the selected `window` (`day`, `week`, `month`, or `all`), oldest first. Extends `/account/equity-history` (equity only, ~1h window) with PnL and volume series across multiple windows; both endpoints derive equity from the same source, so the series never disagree.","operationId":"fetchPortfolioHistory","security":[{"hmacAuth":[]}],"parameters":[{"$ref":"#/components/parameters/PortfolioWindow"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":366},"description":"Maximum number of points to return. Capped server-side at the selected window's capacity (day 288, week 168, month 120, all 366); a larger value is clamped, not rejected. Omit to return the full window."}],"responses":{"200":{"description":"Portfolio time-series for the window, oldest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioHistory"}}}},"400":{"$ref":"#/components/responses/InvalidWindow"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-weight":5,"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Portfolio and equity time series. CCXT has no unified historical-equity method."}},"/positions/closed":{"get":{"tags":["Positions"],"summary":"List closed positions","operationId":"fetchClosedPositions","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":200},"description":"Records per page (default 100, capped at the 200-record retained window). Positions closed in the same millisecond on the same market are returned as one run, so a page can exceed this value — see “Cursor pagination”."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Closed positions, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClosedPosition"}}}},"headers":{"X-Next-Cursor":{"$ref":"#/components/headers/XNextCursor"}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-method":"fetchPositionsHistory"}},"/orders/history":{"get":{"tags":["Account"],"summary":"Order history","description":"Terminal-status order history (filled / cancelled / rejected / expired) for the authenticated account, newest first.","operationId":"fetchOrderHistory","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":500},"description":"Records per page (default 100, capped at the 500-record retained window). Bounds one page, not a cursor walk — see “Cursor pagination”."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Order history, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderHistoryEntry"}}}},"headers":{"X-Next-Cursor":{"$ref":"#/components/headers/XNextCursor"}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-nexus-rate-limit-weight":5,"x-ccxt-method":"fetchOrders"}},"/orders/preview":{"post":{"tags":["Trading"],"summary":"Preview an order","description":"Pre-trade preview: projects the margin/equity/fee impact of an order without submitting it.","operationId":"previewOrder","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"}}}},"responses":{"200":{"description":"Projected pre-trade impact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewResponse"}}}},"400":{"description":"Validation error"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-class":"trading","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Pre-trade simulation. CCXT has no unified preview/dry-run method."}},"/funding":{"get":{"tags":["Funding"],"summary":"Account funding payments","description":"Funding payment history for the authenticated account, newest first.\n\n**This is a bounded window, not the account's whole history.** The rows are served from an in-memory tier, so the answer reaches back only as far as `x-nexus-funding-oldest-ms` says — older rows are persisted durably and are not returned. `x-nexus-funding-truncated` reports the separate question of whether `limit` cut the list. A short answer is not evidence that the account paid no funding.","operationId":"fetchAccountFunding","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":1000},"description":"Maximum records to return"}],"responses":{"200":{"description":"Funding payments, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AccountFunding"}}}},"headers":{"x-nexus-funding-truncated":{"$ref":"#/components/headers/XNexusFundingTruncated"},"x-nexus-funding-oldest-ms":{"$ref":"#/components/headers/XNexusFundingOldestMs"}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-method":"fetchFundingHistory"}},"/deposits":{"post":{"tags":["Account"],"summary":"Submit a deposit","description":"Submit a (testnet/synthetic) deposit for the authenticated account.","operationId":"createDeposit","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepositRequest"},"example":{"amount":"1000","asset":"USDX"}}}},"responses":{"200":{"description":"Deposit acknowledged.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepositResponse"}}}},"400":{"description":"Invalid amount"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Testnet-only balance credit and the internal deposit callback. Not client-facing in CCXT terms."},"get":{"tags":["Account"],"summary":"List deposits","operationId":"fetchDeposits","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":100},"description":"Maximum records to return"}],"responses":{"200":{"description":"Deposit ledger entries, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundsEntry"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-method":"fetchDeposits"}},"/faucet":{"post":{"tags":["Account"],"summary":"Claim testnet faucet","description":"Credit a fixed testnet faucet amount of synthetic USDX to the authenticated account, subject to a per-wallet cooldown and cumulative cap.\n\n**Testnet only.** There is no faucet on mainnet — real-funds collateral arrives through the USDX bridge instead. Do not build a funding flow that assumes this operation exists on every network.","operationId":"claimFaucet","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Testnet-only token dispenser. Not a venue capability CCXT models at all.","x-nexus-network-availability":["testnet","local"],"security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Faucet credited.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FaucetResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"429":{"description":"Cooldown not elapsed or cumulative cap reached"}}}},"/api/v1/markets/summary":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"operationId":"fetchMarketsSummaryV1","tags":["Markets"],"summary":"Market summaries with volume","description":"Returns last trade price, 24h volume, and trade count for all markets.","security":[],"responses":{"200":{"description":"Volume and price summaries for all markets.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MarketSummary"}},"example":[{"market_id":"BTC-USDX-PERP","last_trade_price":48850,"volume_24h":19530020.08,"trade_count":45230}]}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Market summary aggregate. `fetchTickers` is the CCXT-shaped equivalent and is mapped."}},"/api/v1/markets/{market_id}/orderbook":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Order Book"],"summary":"Get order book","operationId":"fetchOrderBookV1","x-ccxt-method":"fetchOrderBook","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"}],"responses":{"200":{"description":"Current order book for the market.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderBook"},"example":{"symbol":"BTC-USDX-PERP","bids":[[50100.5,1.4],[50099,2.1]],"asks":[[50102,0.8],[50103.5,1.2]],"timestamp":1776033930898,"datetime":"2026-04-12T22:45:30.898Z","nonce":1651}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"404":{"description":"Market not found"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/markets/{market_id}/mark-price":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"operationId":"fetchMarkPriceV1","tags":["Markets"],"summary":"Get mark price","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"}],"responses":{"200":{"description":"Current mark price for the market.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarkPriceResponse"},"example":{"market_id":"BTC-USDX-PERP","mark_price":"50011.60"}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"CCXT models mark price as a FIELD, `Ticker.markPrice`, which our `Ticker` schema already declares: `GET /markets/{market_id}/ticker` is badged fetchTicker and `GET /tickers` fetchTickers, so the datum is already reachable through CCXT's own vocabulary. In ccxt 4.5.76 fetchMarkPrices returns Ticker structures keyed by symbol and fetchMarkPrice derives the single-symbol case from it, so badging this route would promise a Ticker from a response that carries one scalar and a market id. Reachable natively as the implicit publicGetMarketsMarketIdMarkPrice."}},"/api/v1/markets/{market_id}/ticker":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Tickers"],"summary":"Get ticker for a market","operationId":"fetchTickerV1","x-ccxt-method":"fetchTicker","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"}],"responses":{"200":{"description":"Current ticker for the market.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ticker"},"example":{"symbol":"BTC-USDX-PERP","timestamp":1776033911836,"datetime":"2026-04-12T22:45:11.836Z","high":50500,"low":49200,"bid":50100.5,"bidVolume":1.4,"ask":50102,"askVolume":0.8,"open":49800,"close":50100,"last":50100,"change":300,"percentage":0.602,"baseVolume":1250.5,"quoteVolume":62525000,"markPrice":50101.5,"indexPrice":null,"info":{}}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"404":{"description":"Market not found"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/markets/{market_id}/trades":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Trades"],"summary":"Get recent trades","operationId":"fetchTradesV1","x-ccxt-method":"fetchTrades","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":10000},"description":"Trades per page (default 100, capped at the 10,000-trade retained window). Bounds one page, not a cursor walk — see “Cursor pagination”."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Recent trades for the market.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Trade"}},"example":[{"id":"cf72c7f3-4c59-4d3c-85c8-99d92bc1fda7","symbol":"BTC-USDX-PERP","price":50100.5,"amount":0.033,"cost":1653.32,"side":"buy","timestamp":1776033942331,"datetime":"2026-04-12T22:45:42.331Z","takerOrMaker":null,"is_liquidation":false,"info":{}}]}},"headers":{"X-Next-Cursor":{"$ref":"#/components/headers/XNextCursor"}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/markets/{market_id}/candles":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Candles"],"summary":"Get OHLCV candles","operationId":"fetchOHLCVV1","x-ccxt-method":"fetchOHLCV","description":"Returns candlestick data as arrays: [timestamp, open, high, low, close, volume], ascending by timestamp. Optionally bounded by `startTime` / `endTime`; unbounded, it returns the latest `limit` bars.\n\n**`startTime` sets the paging direction.** With `startTime` given, the **earliest** `limit` bars at or after it are returned, so the standard `ccxt.fetchOHLCV` loop that advances `since` progresses: each request starts where the last one ended. Without `startTime` — the unbounded or `endTime`-only case — the **most recent** `limit` bars in the window are returned, which is what \"the latest bars\" means above and is unchanged. To reconstruct a long history, pass `startTime` and advance it by the timestamp after the last bar received. This operation has no pagination cursor.","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"},{"name":"timeframe","in":"query","schema":{"type":"string","enum":["1s","1m","5m","1h"],"default":"1m"}},{"$ref":"#/components/parameters/CandleStartTime"},{"$ref":"#/components/parameters/CandleEndTime"},{"name":"limit","in":"query","description":"Maximum number of bars to return. A value above the maximum is clamped rather than rejected.\n\nThe maximum is what the server accepts, not a promise of how many bars exist: the effective ceiling depends on the history available for that market and timeframe, so a response may hold fewer bars than requested. Read `x-nexus-candles-truncated` to tell a capped page from a complete one.","schema":{"type":"integer","default":200,"maximum":5000}}],"responses":{"200":{"description":"OHLCV candles for the market.","content":{"application/json":{"schema":{"type":"array","items":{"type":"array","prefixItems":[{"type":"integer","title":"timestamp","description":"timestamp (ms)"},{"type":"number","title":"open","description":"open"},{"type":"number","title":"high","description":"high"},{"type":"number","title":"low","description":"low"},{"type":"number","title":"close","description":"close"},{"type":"number","title":"volume","description":"volume"}]}},"example":[[1776033900000,48062,51903,44992,51903,27.123]]}},"headers":{"x-nexus-candles-truncated":{"$ref":"#/components/headers/XNexusCandlesTruncated"},"x-nexus-candles-coverage-start-ms":{"$ref":"#/components/headers/XNexusCandlesCoverageStartMs"}}},"400":{"$ref":"#/components/responses/InvalidCandleQuery"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/markets/{market_id}/funding":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"operationId":"fetchFundingV1","tags":["Funding"],"summary":"Get funding rate history","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"},{"name":"limit","in":"query","schema":{"type":"integer","default":300,"maximum":1000}}],"responses":{"200":{"description":"Funding rate history for the market.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundingSample"}},"example":[{"timestamp":1776033960368,"fundingRate":"0.000000016","premium_index":"0.004192","mark_price":"49756.75","oracle_price":"49549.0"}]}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-method":"fetchFundingRateHistory"}},"/api/v1/markets/{market_id}/funding-samples":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Funding"],"summary":"Funding premium-index samples","description":"Dense per-tick premium-index samples (60s cadence, up to 480 points = 8h). Public — no authentication required.","operationId":"fetchFundingSamplesV1","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":480,"maximum":480},"description":"Maximum samples to return (capped at 480)."}],"responses":{"200":{"description":"Premium-index samples, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FundingPremiumSample"}}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Premium-index samples between funding settlements, not funding rates. CCXT's only funding-rate structure is `FundingRateHistory` (a settled `fundingRate` at a timestamp), and `GET /markets/{market_id}/funding` already serves it as fetchFundingRateHistory. `FundingPremiumSample` carries a timestamp and `premium_index`, and states that the settled rate is a property of the settlement window rather than of an intra-window sample, so a fetchFundingRateHistory badge here would publish a `fundingRate` nothing can fill. fetchPremiumIndexOHLCV is the only other candidate in ccxt 4.5.76 and does not describe it either: it returns OHLCV candles, and these are scalar point observations. Reachable natively as the implicit publicGetMarketsMarketIdFundingSamples."}},"/api/v1/markets/{market_id}/status":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"operationId":"fetchMarketStatusV1","tags":["Markets"],"summary":"Get market status and halt info (v0.21)","description":"Returns current market status including halt state from ADL exhaustion. Halted markets reject new orders with ExchangeError::MarketHalted.","security":[],"parameters":[{"$ref":"#/components/parameters/MarketId"}],"responses":{"200":{"description":"Market status and halt information.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketStatus"}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"404":{"description":"Market not found"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus risk parameters and per-market halt state. CCXT carries neither in its unified market shape."}},"/api/v1/tickers":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Tickers"],"summary":"Get tickers for all markets","operationId":"fetchTickersV1","x-ccxt-method":"fetchTickers","security":[],"responses":{"200":{"description":"Object keyed by market_id","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/Ticker"}}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/stats":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Markets"],"summary":"Venue statistics","description":"Aggregate venue statistics plus rolling unique-trader counts. Public — no authentication required.","operationId":"fetchStatsV1","security":[],"responses":{"200":{"description":"Venue statistics snapshot.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsSnapshot"}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Venue-wide analytics. CCXT models per-market data, not venue aggregates."}},"/api/v1/stats/history":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Markets"],"summary":"Venue throughput history","description":"Per-second throughput ring buffer (up to 3600 points). Public — no authentication required.","operationId":"fetchStatsHistoryV1","security":[],"responses":{"200":{"description":"Throughput samples, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ThroughputSample"}}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Venue-wide analytics. CCXT models per-market data, not venue aggregates."}},"/api/v1/orders":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"post":{"tags":["Trading"],"summary":"Submit an order","operationId":"createOrderV1","x-ccxt-method":"createOrder","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"},"examples":{"limit":{"summary":"Plain limit order","value":{"market_id":"BTC-USDX-PERP","side":"Buy","order_type":"Limit","price":"50000","quantity":"0.1","time_in_force":"GTC"}},"stopLimit":{"summary":"Stop-limit — requires trigger_price and a limit price","value":{"market_id":"BTC-USDX-PERP","side":"Sell","order_type":"StopLimit","trigger_price":"48000","price":"47900","quantity":"0.1","time_in_force":"GTC"}},"trailingStop":{"summary":"Trailing stop — market-only, requires trailing_offset_bps","value":{"market_id":"BTC-USDX-PERP","side":"Sell","order_type":"TrailingStop","trailing_offset_bps":250,"quantity":"0.1","time_in_force":"IOC"}},"marketWithSlippageCap":{"summary":"Market order with a server-enforced 50 bp slippage cap","value":{"market_id":"BTC-USDX-PERP","side":"Buy","order_type":"Market","quantity":"0.1","time_in_force":"IOC","max_slippage_bps":50}}}}}},"responses":{"200":{"description":"Idempotent replay: this `client_id` was already accepted, and the body is the order that first request created. Nothing was placed by this request, so branch on the status rather than assuming `201`. `fills` is empty here even if the original order has since traded; read its current state from `GET /orders/{order_id}` (unversioned — the versioned path carries only `DELETE` and `PATCH`).\n\n**Reachable only while the original order is still resting.** The replay answers from the live book, so a fully-filled, cancelled or expired original, and any market/IOC/FOK order that never rested, is answered `409` instead. The duplicate is prevented either way; what differs is whether the original can be handed back inline.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"201":{"description":"Order accepted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderResponse"}}}},"400":{"description":"Validation error (insufficient margin, invalid tick size, etc.)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"409":{"description":"This `client_id` is already claimed, and the order it created is not currently resting, so it cannot be returned inline (`code: DuplicateClientId`). **This is the expected answer whenever the original is no longer on the book**, not a rare edge: a filled, cancelled or expired order, and anything that never rested, all land here. The message names the order's id; find it in `GET /api/v1/orders/history`, which retains terminal orders. Do not retry with the same key, since the outcome will not change, and do not re-submit under a new key without first establishing what the original order did."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-class":"trading"},"get":{"tags":["Account"],"summary":"List open orders","operationId":"fetchOpenOrdersV1","x-ccxt-method":"fetchOpenOrders","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"The account's open orders.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Order"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}},"delete":{"tags":["Trading"],"summary":"Cancel all orders","description":"Charged to the **cancel** budget, which is separate from the order budget that submission and amend draw on (`x-nexus-rate-limit-class: trading` names the class both belong to, not the bucket). A key that has spent its entire submission allowance can still cancel: exhausting one never refuses the other. It is a separate bucket rather than an exemption, so cancelling in a tight loop can still `429` — with `bucket: cancel`, the only refusal that means your cancel channel itself is saturated. See \"Rate limits\" for the full model.","operationId":"cancelAllOrdersV1","x-ccxt-method":"cancelAllOrders","security":[{"hmacAuth":[]}],"parameters":[{"name":"market_id","in":"query","schema":{"type":"string"},"description":"Cancel only orders on this market"}],"responses":{"200":{"description":"The orders that were actually cancelled, in the `Order` shape `GET /orders` serves. A cancel-all that could not reach every market still answers 200 with only the orders it did cancel, so an empty or short array is not proof that nothing rests any more.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Order"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-class":"trading"}},"/api/v1/orders/batch":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"post":{"operationId":"createOrdersBatchV1","tags":["Trading"],"summary":"Submit multiple orders","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderRequest"}}}}},"responses":{"201":{"description":"Per-order results, in request order. Returned with status 201 for the batch as a whole even when individual entries failed; inspect each entry's `error`.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderResult"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"429":{"$ref":"#/components/responses/RateLimited"}},"description":"Submit multiple orders in one request. Orders are processed sequentially and non-atomically: an early order consuming margin can cause a later order in the same batch to fail, and per-order failures do not abort the batch. The response array preserves request order with a per-order success or error result.\n\n**Weighted by batch size.** This call costs `1 + floor(order_count / 40)` units of the trading-action budget, so up to 40 orders cost the same as a single order and every further 40 adds a unit. Batching is therefore cheaper than the equivalent individual submits, and the cost of one call is capped at one second of tokens so a large batch can never be permanently unsatisfiable. See “Rate limits” in the API description.","x-nexus-rate-limit-class":"trading","x-nexus-rate-limit-weight":1,"x-nexus-rate-limit-weight-formula":"1 + floor(order_count / 40)","x-ccxt-method":"createOrders"}},"/api/v1/orders/preview":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"post":{"tags":["Trading"],"summary":"Preview an order","description":"Pre-trade preview: projects the margin/equity/fee impact of an order without submitting it.","operationId":"previewOrderV1","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderRequest"}}}},"responses":{"200":{"description":"Projected pre-trade impact.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreviewResponse"}}}},"400":{"description":"Validation error"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-class":"trading","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Pre-trade simulation. CCXT has no unified preview/dry-run method."}},"/api/v1/orders/{order_id}":{"get":{"tags":["Account"],"summary":"Get order by ID","operationId":"fetchOrderV1","x-ccxt-method":"fetchOrder","security":[{"hmacAuth":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"market_id","in":"query","required":true,"schema":{"type":"string"},"description":"Market the order rests on (required for routing)."}],"responses":{"200":{"description":"The requested order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"No such order for this account: the id was never placed by this account, or it refers to a terminal order older than the 500-per-account retention window. A 404 is deliberately identical for \"never existed\" and \"not yours\" (ownership masking). It no longer means \"not currently resting\" — recently terminal orders return 200 (ENG-10962)."}},"description":"Resolves any order the account placed: resting orders come from the matching engine's authoritative book state, and recently completed orders (filled, cancelled, rejected, expired) are served from the indexer's terminal-order mirror, so an order that fills instantly can still be fetched by the id its placement returned (ENG-10962). The mirror retains the most recent 500 terminal orders per account — the same window `GET /orders/history` lists — so anything listable is fetchable. Older terminal orders resolve only through `GET /orders/history`. This is the `/api/v1` spelling of `GET /orders/{order_id}`; the two are the same operation on the same router (`order_routes()` is nested under `/api/v1` in `api.rs`), so they resolve identically and either may be called. It was the only annotated operation whose versioned twin was missing, which left the alias set with a hole a consumer reading only the `/api/v1` surface would fall into (ENG-13265)."},"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"delete":{"tags":["Trading"],"summary":"Cancel an order","description":"Charged to the **cancel** budget, which is separate from the order budget that submission and amend draw on (`x-nexus-rate-limit-class: trading` names the class both belong to, not the bucket). A key that has spent its entire submission allowance can still cancel: exhausting one never refuses the other. It is a separate bucket rather than an exemption, so cancelling in a tight loop can still `429` — with `bucket: cancel`, the only refusal that means your cancel channel itself is saturated. See \"Rate limits\" for the full model.","operationId":"cancelOrderV1","x-ccxt-method":"cancelOrder","security":[{"hmacAuth":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"market_id","in":"query","required":true,"schema":{"type":"string"},"description":"Market the order rests on (required for routing)."}],"responses":{"200":{"description":"The cancelled order, in the `Order` shape `GET /orders` serves.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Order not found"}},"x-nexus-rate-limit-class":"trading"},"patch":{"tags":["Trading"],"summary":"Amend an order","description":"Atomic cancel-replace amend of a resting order: changes the price and/or size in a single operation. At least one of `price` or `size` must be supplied. Liquidation orders are not amendable, and a pre-trade margin check is applied to the projected replacement before it is accepted. That check excludes the reservation still held by the order being replaced, so an amend is sized on the margin the replacement actually adds rather than on the original and the replacement together: repricing a resting order at the same size needs no additional margin, and shrinking one frees margin rather than requiring it.\n\nAn amend does not restart execution. The replacement carries the original's `filled` forward, so `amount` stays the total you asked for and an order can never execute more than that total, across any number of amends: a Buy 5 that has filled 2 has 3 left to execute both before and after a reprice, and the replacement comes back as `PartiallyFilled` rather than `Open`. `size` sets the new TOTAL quantity, fills included, and must be greater than `filled` — a size at or below it is rejected with InvalidAmend; cancel the order instead.","operationId":"editOrderV1","x-ccxt-method":"editOrder","security":[{"hmacAuth":[]}],"parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"market_id","in":"query","required":true,"schema":{"type":"string"},"description":"Market the order rests on (required for routing)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AmendOrderRequest"},"example":{"price":"50100","size":"0.2"}}}},"responses":{"200":{"description":"Amended order (the replacement, with a fresh id).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"400":{"description":"Invalid amend (empty body, invalid price/size, order not amendable, or margin breach)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"404":{"description":"Order not found"},"409":{"description":"The market-lifecycle admission gate, evaluated on the replacement before the original is touched: `MarketHalted` when the market is halted, `MarketReduceOnly` when the market is in reduce-only and the replacement is neither `reduceOnly` nor a liquidation, and `MarketSuspended` when the market is settling or delisted. Rejected atomically — the original order is left resting, untouched."},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-class":"trading"}},"/api/v1/orders/history":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Order history","description":"Terminal-status order history (filled / cancelled / rejected / expired) for the authenticated account, newest first.","operationId":"fetchOrderHistoryV1","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":500},"description":"Records per page (default 100, capped at the 500-record retained window). Bounds one page, not a cursor walk — see “Cursor pagination”."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Order history, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrderHistoryEntry"}}}},"headers":{"X-Next-Cursor":{"$ref":"#/components/headers/XNextCursor"}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-nexus-rate-limit-weight":5,"x-ccxt-method":"fetchOrders"}},"/api/v1/account":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Get account summary","operationId":"fetchBalanceV1","x-ccxt-method":"fetchBalance","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Account summary for the authenticated caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountSummary"},"example":{"balance":"100000.00","collateral":"100000.00","equity":"102500.50","available_margin":"85000.00","positions":[]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/account/credit":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"post":{"operationId":"creditV1","x-nexus-network-availability":["testnet","local"],"tags":["Account"],"summary":"Claim synthetic USDX credit","description":"Credit synthetic USDX to the authenticated account, up to a per-API-key daily allowance (default 500 USDX, resets at midnight UTC). `amount` is a decimal string; omit it to claim the full remaining daily allowance. Returns 429 with code `daily_limit_exceeded` once the allowance is used up, and 403 with code `credits_frozen` while crediting is administratively frozen. This is a **testnet-only** faucet: the credited USDX is synthetic and carries no real-world value. Mainnet has no synthetic credit — real-funds collateral arrives through the USDX bridge — so do not build a funding flow that assumes this operation exists on every network.","security":[{"hmacAuth":[]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditRequest"},"example":{"amount":"500"}}}},"responses":{"200":{"description":"Credit applied","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditResponse"},"example":{"amount":"500","credited_today":"500","daily_limit":"500"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Refused. Either crediting is administratively frozen (`credits_frozen`), or a jurisdiction control refused the write (`US_RESTRICTED` / `GEO_UNRESOLVED` / `RESTRICTED_JURISDICTION`) — the latter are permanent for the caller's origin and must not be retried. Branch on `code`, which for the jurisdiction reasons equals the `x-nexus-block-reason` header. See “Jurisdiction restrictions” in the API description.","headers":{"x-nexus-block-reason":{"$ref":"#/components/headers/XNexusBlockReason"}},"content":{"application/json":{"examples":{"creditsFrozen":{"summary":"Crediting administratively frozen","value":{"code":"credits_frozen","message":"USDX crediting is temporarily frozen by an administrator. Existing balances remain tradeable."}},"usRestricted":{"summary":"US write restriction","value":{"code":"US_RESTRICTED","message":"This action is not available in the United States or to U.S. persons"}},"geoUnresolved":{"summary":"Origin could not be resolved; the write failed closed","value":{"code":"GEO_UNRESOLVED","message":"Unable to verify request origin; this action is unavailable"}}}}}},"429":{"description":"Daily credit allowance exhausted (resets at midnight UTC)","content":{"application/json":{"example":{"code":"daily_limit_exceeded","message":"daily USDX credit allowance reached for this API key","credited_today":"500","daily_limit":"500"}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Testnet-only balance credit and the internal deposit callback. Not client-facing in CCXT terms."}},"/api/v1/account/deposit-target":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"operationId":"fetchDepositTargetV1","tags":["Account"],"summary":"How to fund this account","description":"Machine-readable funding instructions for the authenticated account, so an autonomous client can fund itself without out-of-band knowledge of how a given deployment accepts collateral.\n\n**The response is a discriminated union on `mode`, and which mode you get is a property of the deployment, not of the request.** Branch on `mode`; do not assume either shape. A deployment with a real deposit-contract address configured answers `onchain`; otherwise it answers `testnet-faucet` and points at its synthetic-credit endpoints. There is no request parameter that selects between them.\n\nAn address is never fabricated to fill the `onchain` shape. A deployment configured for on-chain deposits whose address is malformed refuses with `503` rather than silently falling back to the faucet, because depositing to a bad address burns the funds.\n\n`confirm` is identical in both modes and is the portable primitive: poll `GET /account` until `balance` reflects the funds.\n\nThis operation is **discovery only**. It moves no funds, creates no deposit, and has no side effect — acting on the returned `faucet` or `onchain` instruction is a separate, explicit step by the caller.","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Funding instructions for the authenticated account. Exactly one `mode` is returned; the fields beyond the common ones depend on it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepositTarget"},"examples":{"testnetFaucet":{"summary":"Deployment with no on-chain deposit contract configured","value":{"mode":"testnet-faucet","account":"0x742d35cc6634c0532925a3b844bc9e7595f0beb0","asset":"USDX","min_amount":"10","faucet":{"primary":{"method":"POST","path":"/account/credit","body":{"amount":"10"},"note":"Synthetic test USDX credited off-chain to your exchange balance. Per-API-key allowance (default 500 USDX/day). Omit `amount` to claim the remaining daily allowance."},"alternate":{"method":"POST","path":"/faucet","note":"Fixed per-wallet test USDX grant (default 10000 USDX), once per 24h per wallet."},"disclaimer":"Synthetic test USDX only — no on-chain transfer occurs. The on-chain deposit contract is not yet wired in; this endpoint returns mode=\"onchain\" with a real address once it ships."},"confirm":{"method":"GET","path":"/account","poll_field":"balance","note":"Credit is synchronous (well under the SPEC 30s credit target); poll /account until `balance` reflects the credit before trading."}}},"onchain":{"summary":"Deployment with an on-chain deposit contract configured","value":{"mode":"onchain","account":"0x742d35cc6634c0532925a3b844bc9e7595f0beb0","asset":"USDX","min_amount":"10","onchain":{"chain":"nexus-mainnet","asset":"USDX","address":"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984","token_address":"0xf92b58d2225a73b45ded3bc2290ac1a2077c1cf2","min_amount":"10","instructions":"Approve USDX spend for the deposit contract, then call depositTo(token, amount, beneficiary) with token set to `onchain.token_address`, amount in USDX base units, and beneficiary set to `account`. Only USDX is accepted; other tokens are rejected on-chain. Funds credit to the exchange account within 30s of on-chain confirmation."},"confirm":{"method":"GET","path":"/account","poll_field":"balance","note":"Poll until balance reflects the deposit (SPEC target: within 30s of on-chain confirmation)."}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Refused with `EARLY_ACCESS_REQUIRED`: this deployment restricts funding to early-access participants and the authenticated account is not one. Permanent for that account until it is enrolled — do not retry. The gate deliberately mirrors `POST /account/credit` and `POST /faucet`, so an account that cannot be funded is not told how to fund. Deployments with early access disabled never return it. The contract-wide `RESTRICTED_JURISDICTION` refusal can also surface as a `403` on any operation — see “Jurisdiction restrictions”.","content":{"application/json":{"example":{"code":"EARLY_ACCESS_REQUIRED","message":"Trading is currently restricted to early access participants. Connect the wallet you registered with on the Nexus testnet, or contact support to update your address."}}}},"429":{"$ref":"#/components/responses/RateLimited"},"503":{"description":"`DEPOSIT_TARGET_MISCONFIGURED`: the deployment is configured for on-chain deposits but its deposit-contract address is not a well-formed address, so no funding target can be published. This is an operator misconfiguration rather than a transient fault; retrying does not clear it, and the endpoint deliberately refuses rather than answering `testnet-faucet`, which would mask the bad configuration.","content":{"application/json":{"example":{"code":"DEPOSIT_TARGET_MISCONFIGURED","message":"on-chain deposit address is misconfigured"}}}}},"x-ccxt-method":"fetchDepositAddress"}},"/api/v1/account/fees":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Account fee schedule","description":"The authenticated account's effective fee schedule — maker/taker rate (bps), fee tier, rolling 30-day traded volume, and active discounts — for parity with Hyperliquid `userFees`. This is the forward-looking *schedule* rate, not a realized average: the venue charges a per-fill fee but does not emit a realized per-fill rate. The reported rate's scope is given by `schedule` (see the schema); the account id is taken from the authenticated credentials, not a parameter. Pass `market_id` to request the exact current mirrored schedule for one intended market before the account has traded there. A selected market never falls back to a venue-modal or unrelated market rate: an unavailable or stale target returns `schedule: unknown`, zero sentinels, and an empty `markets` array.","operationId":"fetchAccountFeesV1","parameters":[{"name":"market_id","in":"query","required":false,"schema":{"type":"string","pattern":"^[A-Z0-9]+(-[A-Z0-9]+)*$","maxLength":64},"example":"BTC-USDX-PERP","description":"Optional exact target for an account fee preflight. When present and currently mirrored, the response contains exactly this market in `markets` and uses its maker/taker pair as the headline. A well-formed target whose fee parameters are unavailable or stale returns HTTP 200 with `schedule: unknown`, zero headline sentinels, and no rows; it never falls back to an unrelated venue reference. A malformed value, including supplying the scalar parameter more than once, is rejected with `400` (`INVALID_MARKET_ID`). The raw query string is part of the HMAC canonical request and must be signed exactly as sent."}],"security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Account fee schedule. With `market_id`, a known target is the sole `markets` row and supplies the headline pair; an unavailable or stale target fails closed as `unknown` rather than substituting another market's rate.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountFees"},"example":{"maker_fee_bps":-2,"taker_fee_bps":5,"tier":"base","schedule":"per_market","markets":[{"symbol":"BTC-USDX-PERP","maker_fee_bps":-2,"taker_fee_bps":5},{"symbol":"ETH-USDX-PERP","maker_fee_bps":-2,"taker_fee_bps":5}],"volume_30d":"101005.00","volume_30d_estimated":false,"discounts":[]}}}},"400":{"$ref":"#/components/responses/InvalidMarketId"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-method":"fetchTradingFees"}},"/api/v1/account/summary":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Account portfolio summary","operationId":"fetchAccountSummaryV1","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Portfolio summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountPortfolioSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/AuthoritativeMarginUnavailable"}},"x-nexus-rate-limit-weight":5,"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus account aggregates. `fetchBalance` is the CCXT-shaped view; these are richer and unmapped by design."}},"/api/v1/account/state":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Consolidated account state","description":"Full account state in a single call: the portfolio summary aggregates plus all open positions (`{ summary, positions }`). Saves clients from pairing `/account/summary` with `/positions`, matching Hyperliquid `clearinghouseState` ergonomics. Both parts are built from one coherent read, so `summary.open_positions_count` always matches the `positions` length, and the embedded `summary` is identical to the standalone `/account/summary` response. Fails closed with `502` when the engine-authoritative margin view is unavailable.","operationId":"fetchAccountStateV1","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Consolidated account state (summary aggregates + open positions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountState"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"$ref":"#/components/responses/AuthoritativeMarginUnavailable"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus account aggregates. `fetchBalance` is the CCXT-shaped view; these are richer and unmapped by design."}},"/api/v1/account/equity-history":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Account equity history","description":"Equity time-series for the authenticated account (5s cadence, ~1h window), oldest first.","operationId":"fetchEquityHistoryV1","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":720},"description":"Points per page (default 100, capped at the 720-point retained window). Points sharing one sample millisecond are returned as one run, so a page can exceed this value — see “Cursor pagination”."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Equity samples, oldest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EquityPoint"}}}},"headers":{"X-Next-Cursor":{"$ref":"#/components/headers/XNextCursor"}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Portfolio and equity time series. CCXT has no unified historical-equity method."}},"/api/v1/account/portfolio-history":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Account portfolio time-series","description":"Portfolio time-series for the authenticated account — equity, cumulative trading PnL, and cumulative traded volume — downsampled over the selected `window` (`day`, `week`, `month`, or `all`), oldest first. Extends `/account/equity-history` (equity only, ~1h window) with PnL and volume series across multiple windows; both endpoints derive equity from the same source, so the series never disagree.","operationId":"fetchPortfolioHistoryV1","security":[{"hmacAuth":[]}],"parameters":[{"$ref":"#/components/parameters/PortfolioWindow"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":366},"description":"Maximum number of points to return. Capped server-side at the selected window's capacity (day 288, week 168, month 120, all 366); a larger value is clamped, not rejected. Omit to return the full window."}],"responses":{"200":{"description":"Portfolio time-series for the window, oldest first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PortfolioHistory"}}}},"400":{"$ref":"#/components/responses/InvalidWindow"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-weight":5,"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Portfolio and equity time series. CCXT has no unified historical-equity method."}},"/api/v1/account/rate-limit":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Get rate limit status","description":"Returns the authenticated caller's rate limit tier and, per budget, the ceiling, tokens remaining and reset timestamp — the same data surface as the X-RateLimit-* response headers, exposed as a queryable resource. This endpoint does not consume a rate limit token, so it can be polled freely to self-manage pacing without depleting the caller's budget. The top-level `limit`, `remaining` and `reset_at_ms` report the **request** class, which for an HMAC key is the binding minimum of the key and owner buckets; for unlimited-tier callers (gateway keys) they are null. `buckets` additionally reports each budget on its own — including `order` and `cancel`, the pools order writes and cancellations are actually charged to — keyed by the same labels a `429` uses, so a client can read its order-placement headroom rather than inferring it from the read budget. WebSocket ceilings are not reported here. See “Rate limits” in the API description.","operationId":"fetchRateLimitStatusV1","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Current rate-limit status for the caller.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitStatus"},"example":{"tier":"pro","limit":20,"remaining":17,"reset_at_ms":1765432100123,"buckets":{"key":{"limit":20,"remaining":17,"reset_at_ms":1765432100123},"owner":{"limit":20,"remaining":19,"reset_at_ms":1765432100051},"order":{"limit":20,"remaining":20,"reset_at_ms":0},"cancel":{"limit":20,"remaining":20,"reset_at_ms":0}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Rate-limit introspection. CCXT models rate limits client-side from `describe()`, not by asking the venue."}},"/api/v1/account/cancel-on-disconnect":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Get cancel-on-disconnect status","description":"Returns the authenticated account's cancel-on-disconnect (COD) status. COD is an opt-in, per-account dead man's switch: when the account's last authenticated `/ws` connection drops and does not reconnect within the grace window, the exchange automatically cancels all of the account's resting orders, so a crashed client cannot leave orders exposed. `enabled` is the account's own opt-in; `active` additionally requires the exchange-side feature switch, so it reflects whether COD will actually fire; `grace_secs` is the exchange-configured reconnect window in seconds (null when the feature is unavailable). Clients that trade purely over REST and never open a `/ws` connection are not covered.","operationId":"fetchCancelOnDisconnectV1","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Current cancel-on-disconnect status for the account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelOnDisconnectStatus"},"example":{"enabled":true,"active":true,"grace_secs":10}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Cancel-on-disconnect is a Nexus session policy, not a CCXT method."},"put":{"tags":["Account"],"summary":"Set cancel-on-disconnect","description":"Enables or disables cancel-on-disconnect for the authenticated account. Opt-in is per account and off by default: someone who deliberately leaves a passive resting order while offline should not have a brief blip cancel it. Enable it when you want the guarantee that a dead client cannot keep orders resting — typical for market makers and algorithmic traders. Returns the resulting COD status (same shape as the GET).","operationId":"setCancelOnDisconnectV1","security":[{"hmacAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetCancelOnDisconnectRequest"},"example":{"enabled":true}}}},"responses":{"200":{"description":"The resulting cancel-on-disconnect status for the account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelOnDisconnectStatus"},"example":{"enabled":true,"active":true,"grace_secs":10}}}},"400":{"description":"Malformed request body."},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Cancel-on-disconnect is a Nexus session policy, not a CCXT method."}},"/api/v1/positions":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Positions"],"summary":"List open positions","operationId":"fetchPositionsV1","x-ccxt-method":"fetchPositions","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"The account's open positions.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Position"}},"example":[{"market_id":"BTC-USDX-PERP","side":"Long","size":"0.5","entry_price":"49500.00","unrealized_pnl":"250.50","realized_pnl":"0.00","liquidation_price":null,"liquidation_price_error":"margin_state_not_mirrored","notional_value":"25000.50","notional_value_error":null,"roe":"0.2004","roe_error":null,"margin_used":"1250.03","margin_used_error":null,"max_leverage":20,"max_leverage_error":null,"funding_paid":"12.50","leverage":null,"leverage_error":"margin_state_not_mirrored"}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/positions/closed":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Positions"],"summary":"List closed positions","operationId":"fetchClosedPositionsV1","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":200},"description":"Records per page (default 100, capped at the 200-record retained window). Positions closed in the same millisecond on the same market are returned as one run, so a page can exceed this value — see “Cursor pagination”."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Closed positions, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClosedPosition"}}}},"headers":{"X-Next-Cursor":{"$ref":"#/components/headers/XNextCursor"}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-ccxt-method":"fetchPositionsHistory"}},"/api/v1/positions/pnl":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Positions"],"summary":"Per-position P&L decomposition","operationId":"fetchPositionsPnlV1","x-nexus-rate-limit-weight":5,"description":"Each open position's P&L split into price, funding and fee components. One entry per open position; an account with none gets an empty array.\n\n**Read the sign note on `PositionPnl.funding_pnl` before using it alongside `/positions`.** The two operations report the same funding cash flow with opposite signs, deliberately.\n\nServed from indexer-local projections, so it does not touch the engine and is safe to poll independently of `/positions`.\n\nThe `/api/v1` spelling of the same operation, served by the direct indexer mount. `account_position_routes()` mounts this route at both the legacy root path and this prefix, so the two are the same handler and the same cost.","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"The account's open positions, with P&L decomposed.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PositionPnl"}},"example":[{"market_id":"BTC-USDX-PERP","side":"Long","size":"0.5","entry_pnl":"250.50","unrealized_pnl":"250.50","realized_pnl":"0","funding_pnl":"-3.21","fee_pnl":"-6.25","total_pnl":"241.04","total_pnl_complete":true}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Unrealised PnL breakdown. CCXT carries PnL inside the position shape, so this has no separate method."}},"/api/v1/fills":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"operationId":"fetchFillsV1","tags":["Account"],"summary":"List your fills","description":"Returns up to 1000 fills for the authenticated account, newest first. Fills are trade executions resulting from order matches — each fill carries the matched price, quantity, fee, and whether it was the taker or maker side.","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":1000},"description":"Fills per page (default 100, capped at the 1,000-fill retained window). Bounds one page, not a cursor walk — see “Cursor pagination”."},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"description":"Array of fills, newest first","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Fill"}},"example":[{"id":"cf72c7f3-1234-5678-abcd-ef0123456789","order_id":"ord_a1b2c3d4e5f6...","market_id":"BTC-USDX-PERP","side":"buy","price":"84250.00","size":"0.01","fee":"0.84","taker_or_maker":"taker","timestamp":1779225381434,"is_liquidation":false}]}},"headers":{"X-Next-Cursor":{"$ref":"#/components/headers/XNextCursor"}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-nexus-rate-limit-weight":5,"x-ccxt-method":"fetchMyTrades"}},"/api/v1/bridge/deposits":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Bridge"],"summary":"List bridge deposits","description":"List the authenticated account's cross-chain deposits, newest first. Read-only: the watcher creates and advances these records, and this endpoint only reads them.\n\nThere is no pre-arrival state. A deposit exists only once the watcher observes an on-chain transfer, so every record carries a `tx_hash` and `log_index` (its id is `{tx_hash}:{log_index}`) and a confirmation count. A fiat-funded payment still in provider checkout is not yet a deposit and does not appear here.\n\nOptional filters narrow by `chain`, `asset` and `status`; an unrecognized filter value simply matches nothing. Deposits are USDC or USDX only in this cut.\n\nThe list returns at most the newest 100 deposits and is not yet paginated: there is no cursor or time-window parameter, so an account with more than 100 deposits can only reach older ones by narrowing `chain`, `asset` or `status`. A cursor may be added later; doing so is additive.","operationId":"listBridgeDeposits","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus-specific asset bridging. CCXT's deposit/withdraw methods model a venue's own custody, not a cross-chain bridge, and mapping them would misdescribe both.","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":100,"minimum":1},"description":"Maximum records to return, 1-100. Values above the maximum are clamped to it; a non-integer or zero value is a 400."},{"name":"chain","in":"query","required":false,"schema":{"type":"string"},"description":"Filter by source chain, e.g. `ethereum`."},{"name":"asset","in":"query","required":false,"schema":{"type":"string","enum":["USDC","USDX"]},"description":"Filter by deposited asset."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["detected","confirming","credited","failed","reverted"]},"description":"Filter by deposit status."}],"responses":{"200":{"description":"Bridge deposits, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BridgeDeposit"}}}}},"400":{"$ref":"#/components/responses/BridgeBadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/bridge/deposits/{id}":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Bridge"],"summary":"Get a bridge deposit","description":"Fetch a single cross-chain deposit by id. The id is the watcher's dedup key, `{tx_hash}:{log_index}`. Only deposits owned by the authenticated account are returned; an id belonging to another account is a `404`, not a `403`, so ids are not account-probing oracles.","operationId":"getBridgeDeposit","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus-specific asset bridging. CCXT's deposit/withdraw methods model a venue's own custody, not a cross-chain bridge, and mapping them would misdescribe both.","security":[{"hmacAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Deposit identifier, `{tx_hash}:{log_index}`."}],"responses":{"200":{"description":"The deposit.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeDeposit"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/BridgeNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/bridge/withdrawals":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"post":{"tags":["Bridge"],"summary":"Trigger a bridge withdrawal","description":"Trigger a withdrawal to a Halliday one-time wallet (OTW). The server quotes an OTW from Halliday, debits the account's USDX on the Exchange, and releases the funds on-chain to that OTW; Halliday then settles to the user in the asset and destination they chose.\n\nThe request carries only an `amount` (USDX base units). There is no `destination_address` -- the destination is the OTW Halliday quotes, never a user-supplied address.\n\nThe write path runs synchronously and the response carries the resulting record with its terminal status. The `Idempotency-Key` header is required and makes the call safe to retry: the same key returns the original withdrawal without moving money again. A replay returns the stored record as `200` even when it is terminal `failed` (including a withdrawal the original call rejected with a `400`, e.g. insufficient balance), so branch on the returned `status`, not on the HTTP code, and expect a key consumed by a terminal `failed` to keep replaying that failure.\n\nStates advance pending -> quoted -> broadcast -> confirmed | failed. On `failed`, the Exchange debit is reversed only when the release is provably unpaid (bad config, a terminally-rejected broadcast, or a mined-and-reverted tx); when the release was broadcast and its outcome is unknown the debit is left in place for reconciliation, so `failed` does not by itself mean the balance was restored -- read `failure_reason`.\n\nThe debit can also be rejected synchronously: a `400` when the account has insufficient balance, or an open position or open orders that must be closed first; a `503` when the Exchange is not currently accepting withdrawals. Neither applies a debit.","operationId":"createBridgeWithdrawal","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus-specific Halliday-fronted payout. CCXT's withdraw method models a venue paying a user-supplied address from its own custody, not a per-withdrawal one-time-wallet quote settled by a third party.","security":[{"hmacAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string"},"description":"Client-generated key that makes the withdrawal idempotent. Retrying with the same key returns the original withdrawal without triggering a second payout."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeWithdrawalRequest"}}}},"responses":{"200":{"description":"Idempotent replay: the withdrawal this Idempotency-Key already created. May be terminal `failed` -- even a withdrawal the original call rejected with a 400 replays here as 200 -- so branch on its `status`, not the HTTP code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeWithdrawal"}}}},"201":{"description":"The withdrawal was triggered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeWithdrawal"}}}},"400":{"$ref":"#/components/responses/BridgeBadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/RestrictedJurisdiction"},"409":{"description":"The Idempotency-Key was already used with a different amount. The key is bound to its first request, so a mismatched retry is rejected rather than silently replaying the original withdrawal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"description":"A downstream step failed (Halliday quote, the Exchange debit, or the on-chain release). On a release failure the debit is reversed only when the failure is provably money-safe; an unknown on-chain outcome leaves the account debited pending reconciliation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeError"}}}},"503":{"description":"Withdrawals are not available: either not enabled on this deployment (the money path is not provisioned), or the Exchange is not currently accepting withdrawals (withdrawals disabled, or an uncovered system loss outstanding). Transient — no debit was applied; retry once the condition clears.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeError"}}}}}},"get":{"tags":["Bridge"],"summary":"List bridge withdrawals","description":"List the authenticated account's withdrawals, newest first. Optional `status` filter; an unrecognized value simply matches nothing. Returns at most the newest 100 and is not yet paginated.","operationId":"listBridgeWithdrawals","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus-specific Halliday-fronted payout. CCXT's withdraw method models a venue paying a user-supplied address from its own custody, not a per-withdrawal one-time-wallet quote settled by a third party.","security":[{"hmacAuth":[]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"maximum":100,"minimum":1},"description":"Maximum records to return, 1-100. Values above the maximum are clamped; a non-integer or zero value is a 400."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","quoted","broadcast","confirmed","failed"]},"description":"Filter by withdrawal status."}],"responses":{"200":{"description":"Bridge withdrawals, newest first.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BridgeWithdrawal"}}}}},"400":{"$ref":"#/components/responses/BridgeBadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/bridge/withdrawals/{id}":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Bridge"],"summary":"Get a bridge withdrawal","description":"Fetch a single withdrawal by id, scoped to the authenticated account.","operationId":"getBridgeWithdrawal","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus-specific Halliday-fronted payout. CCXT's withdraw method models a venue paying a user-supplied address from its own custody, not a per-withdrawal one-time-wallet quote settled by a third party.","security":[{"hmacAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"The withdrawal id (wdl_<hex>)."}],"responses":{"200":{"description":"The withdrawal.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeWithdrawal"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/BridgeNotFound"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/bridge/assets":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Bridge"],"summary":"List bridgeable chains and assets","description":"Returns the supported chains and, per chain, the depositable assets (USDC, USDX) with their decimals, minimum amount and required confirmations, and the withdrawable assets (USDX) with their minimum amount and flat fee. Metadata only — it moves no funds and reads no account state, but carries the standard `/v1/bridge` HMAC auth like the rest of the surface.\n\nAsset scope is USDC and USDX; USDT is out of scope for this cut. `chain_id` is the serving network's numbered chain (`1` on mainnet, `11155111`/Sepolia on testnet) and is `null` on a local instance that serves no numbered chain. Arbitrum becomes another `chains[]` entry later with no shape change.\n\nThe published `min_amount` is provisional: the delivered-amount minimum is still being reconciled against the provider's quoted limits (ENG-8297), because a fixed-input funding route can deliver net of fees below a quote-time minimum. Treat it as a floor that may rise, not a settled figure.","operationId":"getBridgeAssets","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Nexus-specific asset bridging. CCXT's deposit/withdraw methods model a venue's own custody, not a cross-chain bridge, and mapping them would misdescribe both.","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Supported bridge chains and their deposit/withdraw assets.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeAssetsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/positions/pnl":{"get":{"tags":["Positions"],"summary":"Per-position P&L decomposition","operationId":"fetchPositionsPnl","x-nexus-rate-limit-weight":5,"description":"Each open position's P&L split into price, funding and fee components. One entry per open position; an account with none gets an empty array.\n\n**Read the sign note on `PositionPnl.funding_pnl` before using it alongside `/positions`.** The two operations report the same funding cash flow with opposite signs, deliberately.\n\nServed from indexer-local projections, so it does not touch the engine and is safe to poll independently of `/positions`.","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"The account's open positions, with P&L decomposed.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PositionPnl"}},"example":[{"market_id":"BTC-USDX-PERP","side":"Long","size":"0.5","entry_pnl":"250.50","unrealized_pnl":"250.50","realized_pnl":"0","funding_pnl":"-3.21","fee_pnl":"-6.25","total_pnl":"241.04","total_pnl_complete":true}]}}},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Unrealised PnL breakdown. CCXT carries PnL inside the position shape, so this has no separate method."}},"/account/funding-snapshot":{"get":{"tags":["Account"],"summary":"Authoritative account funding snapshot","description":"Read the risk-owned open and closed-pending funding obligations and actual settlement cash for one authenticated account and market. The response is one atomic engine read, not an indexer-history reconstruction. Unsampled or unavailable sources return explicit status and nullable values. A missing old-engine route, malformed body, mismatched identity, stale observation or failed backend returns 502. Existing /funding remains settled history. The raw query is part of the HMAC signature.","operationId":"fetchAccountFundingSnapshot","parameters":[{"name":"market_id","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":128},"example":"SOL-USDX-PERP"}],"security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Coherent snapshot with explicit completeness and sample boundary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountFundingSnapshot"}}}},"400":{"description":"Missing, duplicate or unsupported query parameters, or invalid market_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountFundingRequestError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The authoritative engine reports MarketNotFound for the requested market. An old engine without this route returns 502 instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountFundingRequestError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"description":"FUNDING_SOURCE_UNAVAILABLE, FUNDING_SOURCE_INVALID or FUNDING_SOURCE_STALE; no trustworthy snapshot can be returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountFundingSourceError"}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"CCXT has no unified pending funding obligation / cumulative settlement snapshot method. Native implicit privateGetAccountFundingSnapshot preserves this richer data; fetchFundingHistory denotes settled payment records and is not equivalent."}},"/api/v1/account/funding-snapshot":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Authoritative account funding snapshot","description":"Read the risk-owned open and closed-pending funding obligations and actual settlement cash for one authenticated account and market. The response is one atomic engine read, not an indexer-history reconstruction. Unsampled or unavailable sources return explicit status and nullable values. A missing old-engine route, malformed body, mismatched identity, stale observation or failed backend returns 502. Existing /funding remains settled history. The raw query is part of the HMAC signature.","operationId":"fetchAccountFundingSnapshotV1","parameters":[{"name":"market_id","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":128},"example":"SOL-USDX-PERP"}],"security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Coherent snapshot with explicit completeness and sample boundary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountFundingSnapshot"}}}},"400":{"description":"Missing, duplicate or unsupported query parameters, or invalid market_id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountFundingRequestError"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"The authoritative engine reports MarketNotFound for the requested market. An old engine without this route returns 502 instead.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountFundingRequestError"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"502":{"description":"FUNDING_SOURCE_UNAVAILABLE, FUNDING_SOURCE_INVALID or FUNDING_SOURCE_STALE; no trustworthy snapshot can be returned.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountFundingSourceError"}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"CCXT has no unified pending funding obligation / cumulative settlement snapshot method. Native implicit privateGetAccountFundingSnapshot preserves this richer data; fetchFundingHistory denotes settled payment records and is not equivalent."}},"/account/referrals":{"get":{"tags":["Account"],"summary":"Account referrals summary","description":"The authenticated account's own referral code and how many accounts used it. The account id is taken from the authenticated credentials, not a parameter — same convention as GET /account/fees.","operationId":"fetchAccountReferrals","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Referral program metadata. CCXT has no unified method for a venue's own referral program.","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Account referrals summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountReferralsSummary"},"example":{"code":"9f2c3a1b4d5e6f708192a3","referred_count":3,"active_referred_count":1}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"502":{"description":"`BAD_GATEWAY` — the accounts-directory backend is reachable but the summary request to it failed. Retryable; not evidence the referral relationship itself is wrong.","content":{"application/json":{"example":{"code":"BAD_GATEWAY","message":"upstream unavailable"}}}},"503":{"description":"`REFERRAL_STORE_NOT_CONFIGURED` — this deployment has no accounts-directory backend configured. Not a per-account condition; every call fails the same way until the deployment is configured.","content":{"application/json":{"example":{"code":"REFERRAL_STORE_NOT_CONFIGURED","message":"accounts-access backend is not configured"}}}}}}},"/api/v1/account/referrals":{"servers":[{"url":"https://api.testnet.nexus.xyz","description":"Public testnet (play funds) — host-root base for the full /api/v1 path. Requires the public testnet route promotion; verify deployment before use."},{"url":"http://localhost:9090","description":"Local development — not a public network."}],"get":{"tags":["Account"],"summary":"Account referrals summary","description":"The authenticated account's own referral code and how many accounts used it. The account id is taken from the authenticated credentials, not a parameter — same convention as GET /account/fees.","operationId":"fetchAccountReferralsV1","x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Referral program metadata. CCXT has no unified method for a venue's own referral program.","security":[{"hmacAuth":[]}],"responses":{"200":{"description":"Account referrals summary.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountReferralsSummary"},"example":{"code":"9f2c3a1b4d5e6f708192a3","referred_count":3,"active_referred_count":1}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"502":{"description":"`BAD_GATEWAY` — the accounts-directory backend is reachable but the summary request to it failed. Retryable; not evidence the referral relationship itself is wrong.","content":{"application/json":{"example":{"code":"BAD_GATEWAY","message":"upstream unavailable"}}}},"503":{"description":"`REFERRAL_STORE_NOT_CONFIGURED` — this deployment has no accounts-directory backend configured. Not a per-account condition; every call fails the same way until the deployment is configured.","content":{"application/json":{"example":{"code":"REFERRAL_STORE_NOT_CONFIGURED","message":"accounts-access backend is not configured"}}}}}}},"/transfers":{"post":{"operationId":"createOrdinaryTransfer","tags":["Account"],"summary":"Transfer USDX to another ordinary account","description":"Atomically debit the source and credit the destination, conserving venue USDX. Both accounts must first enroll by proving their own keys. Distinct ordinary accounts only; no system accounts, agents, keyless subaccounts or vaults. Source must be flat with no reservations or pending funding. Transfers honor existing outflow controls; an already-issued gateway permit may finish within five seconds of a freeze. A 200 receipt is returned only after both cash legs and the ledger posting are durable. Retry with a fresh owner nonce and the same client_transfer_id and identical economic intent to recover the same receipt without another debit. Changing that intent under an existing id returns 409. Receipt history survives snapshot/WAL recovery; the first release retains up to 100000 venue receipts without evicting idempotency keys, then refuses new payments. Both balance events include the same transfer_id.\n\nOwner consent uses recoverable secp256k1 (65-byte hex r || s || v; low-S, v 0/1 or 27/28). Sign Keccak256 of UTF-8 `{METHOD-uppercase}\\n{canonical-root-path}\\n{exact-query-without-?}\\n{hex-lowercase-SHA256(exact-body-bytes)}\\n{timestamp-ms}\\n{nonce}`. Both root and /api/v1 paths use the canonical root path. Do not reserialize a signed body. The deployment domain is signed in the body for POST and in the query for GET. HMAC/API-key, bearer-only and agent authority cannot authorize these operations. The server sets Cache-Control: no-store.","security":[{"ownerSignature":[]}],"parameters":[{"in":"header","name":"X-Timestamp","required":true,"schema":{"type":"string","pattern":"^[0-9]+$","maxLength":20},"description":"Unix milliseconds, within 30 seconds of server time."},{"in":"header","name":"X-Nonce","required":true,"schema":{"type":"string","pattern":"^[0-9]+$","maxLength":20},"description":"Unsigned 64-bit owner nonce. POST consumes a strictly increasing nonce durably even if later settlement fails; sign a fresh higher nonce when retrying the same client transfer id. GET consumes no nonce."}],"responses":{"200":{"description":"Committed result or authoritative read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferReceipt"}}}},"400":{"description":"INVALID_TRANSFER_REQUEST for invalid intent/domain/precision; TRANSFER_RISK_REJECTED for insufficient cash, open exposure, liquidation or system-loss constraints.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"401":{"description":"TRANSFER_SIGNATURE_REQUIRED: missing, forged, stale, agent or replayed POST owner proof. No additional payment was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"403":{"description":"TRANSFER_ACCOUNT_INELIGIBLE or TRANSFER_OUTFLOW_BLOCKED: ordinary enrollment or live source controls do not permit this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"409":{"description":"TRANSFER_ID_CONFLICT: source/client_transfer_id already identifies different economic intent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"502":{"description":"TRANSFER_OUTCOME_UNKNOWN: upstream response was lost or unavailable. Query the original client transfer id before retrying; never invent a replacement payment id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"503":{"description":"TRANSFERS_UNAVAILABLE: disabled/unconfigured service, unavailable durable writer, invalid/expired control permit, pending committed fills or receipt capacity reached. No successful outcome is implied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferRequest"}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Ordinary cross-owner payments use a recoverable owner signature and enrollment. CCXT transfer moves funds between balances of the same owner; no CCXT custody/signing adapter is shipped for this operation."},"get":{"operationId":"listOrdinaryTransfers","tags":["Account"],"summary":"Read ordinary USDX transfer history","description":"Authoritative ascending-id history where the signed account is sender or recipient. This endpoint reads durable engine receipts, not an indexer cache. It remains readable when new transfers are disabled on a configured deployment. The optional client_transfer_id lookup is source-scoped and resolves ambiguous submissions in constant time. GET bodies must be empty. Sign the exact query, including pagination. Neither party learns the other account's total balance.\n\nOwner consent uses recoverable secp256k1 (65-byte hex r || s || v; low-S, v 0/1 or 27/28). Sign Keccak256 of UTF-8 `{METHOD-uppercase}\\n{canonical-root-path}\\n{exact-query-without-?}\\n{hex-lowercase-SHA256(exact-body-bytes)}\\n{timestamp-ms}\\n{nonce}`. Both root and /api/v1 paths use the canonical root path. Do not reserialize a signed body. The deployment domain is signed in the body for POST and in the query for GET. HMAC/API-key, bearer-only and agent authority cannot authorize these operations. The server sets Cache-Control: no-store.","security":[{"ownerSignature":[]}],"parameters":[{"in":"header","name":"X-Timestamp","required":true,"schema":{"type":"string","pattern":"^[0-9]+$","maxLength":20},"description":"Unix milliseconds, within 30 seconds of server time."},{"in":"header","name":"X-Nonce","required":true,"schema":{"type":"string","pattern":"^[0-9]+$","maxLength":20},"description":"Unsigned 64-bit owner nonce. POST consumes a strictly increasing nonce durably even if later settlement fails; sign a fresh higher nonce when retrying the same client transfer id. GET consumes no nonce."},{"in":"query","name":"account","required":true,"schema":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"description":"Owner account signing this read."},{"in":"query","name":"domain","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$"},"description":"Deployment domain, included in the signature."},{"in":"query","name":"after","schema":{"type":"integer","format":"int64","minimum":0,"default":0},"description":"Exclusive receipt cursor."},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"in":"query","name":"client_transfer_id","schema":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$"},"description":"Optional sender-only lookup; returns zero or one receipt and overrides pagination."}],"responses":{"200":{"description":"Committed result or authoritative read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferHistory"}}}},"400":{"description":"INVALID_TRANSFER_REQUEST for invalid intent/domain/precision; TRANSFER_RISK_REJECTED for insufficient cash, open exposure, liquidation or system-loss constraints.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"401":{"description":"TRANSFER_SIGNATURE_REQUIRED: missing, forged, stale, agent or replayed POST owner proof. No additional payment was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"403":{"description":"TRANSFER_ACCOUNT_INELIGIBLE or TRANSFER_OUTFLOW_BLOCKED: ordinary enrollment or live source controls do not permit this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"502":{"description":"TRANSFER_READ_UNAVAILABLE: retry the signed read; no payment was submitted by this read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"503":{"description":"TRANSFERS_UNAVAILABLE: disabled/unconfigured service, unavailable durable writer, invalid/expired control permit, pending committed fills or receipt capacity reached. No successful outcome is implied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Ordinary cross-owner payments use a recoverable owner signature and enrollment. CCXT transfer moves funds between balances of the same owner; no CCXT custody/signing adapter is shipped for this operation."}},"/transfers/{id}":{"get":{"operationId":"getOrdinaryTransfer","tags":["Account"],"summary":"Read a committed ordinary transfer","description":"The sender and recipient can read the same immutable receipt. An unrelated valid signer receives 404, as does an unknown id. Sign the exact query and use an empty GET body.\n\nOwner consent uses recoverable secp256k1 (65-byte hex r || s || v; low-S, v 0/1 or 27/28). Sign Keccak256 of UTF-8 `{METHOD-uppercase}\\n{canonical-root-path}\\n{exact-query-without-?}\\n{hex-lowercase-SHA256(exact-body-bytes)}\\n{timestamp-ms}\\n{nonce}`. Both root and /api/v1 paths use the canonical root path. Do not reserialize a signed body. The deployment domain is signed in the body for POST and in the query for GET. HMAC/API-key, bearer-only and agent authority cannot authorize these operations. The server sets Cache-Control: no-store.","security":[{"ownerSignature":[]}],"parameters":[{"in":"header","name":"X-Timestamp","required":true,"schema":{"type":"string","pattern":"^[0-9]+$","maxLength":20},"description":"Unix milliseconds, within 30 seconds of server time."},{"in":"header","name":"X-Nonce","required":true,"schema":{"type":"string","pattern":"^[0-9]+$","maxLength":20},"description":"Unsigned 64-bit owner nonce. POST consumes a strictly increasing nonce durably even if later settlement fails; sign a fresh higher nonce when retrying the same client transfer id. GET consumes no nonce."},{"in":"path","name":"id","required":true,"schema":{"type":"integer","format":"int64","minimum":1}},{"in":"query","name":"account","required":true,"schema":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"description":"Owner account signing this read."},{"in":"query","name":"domain","required":true,"schema":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$"},"description":"Deployment domain, included in the signature."}],"responses":{"200":{"description":"Committed result or authoritative read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferReceipt"}}}},"400":{"description":"INVALID_TRANSFER_REQUEST for invalid intent/domain/precision; TRANSFER_RISK_REJECTED for insufficient cash, open exposure, liquidation or system-loss constraints.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"401":{"description":"TRANSFER_SIGNATURE_REQUIRED: missing, forged, stale, agent or replayed POST owner proof. No additional payment was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"403":{"description":"TRANSFER_ACCOUNT_INELIGIBLE or TRANSFER_OUTFLOW_BLOCKED: ordinary enrollment or live source controls do not permit this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"404":{"description":"TRANSFER_NOT_FOUND: id is absent or not visible to the signed account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"502":{"description":"TRANSFER_READ_UNAVAILABLE: retry the signed read; no payment was submitted by this read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"503":{"description":"TRANSFERS_UNAVAILABLE: disabled/unconfigured service, unavailable durable writer, invalid/expired control permit, pending committed fills or receipt capacity reached. No successful outcome is implied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Ordinary cross-owner payments use a recoverable owner signature and enrollment. CCXT transfer moves funds between balances of the same owner; no CCXT custody/signing adapter is shipped for this operation."}},"/transfers/enroll":{"post":{"operationId":"enrollOrdinaryTransferAccount","tags":["Account"],"summary":"Enroll an ordinary transfer account","description":"Prove ownership of an ordinary 20-byte account address to enroll it for sending and receiving USDX. This records the owner grant and nonce durably. It does not fund the account or grant control over another account and cannot clear an outflow freeze. Sign with the enrolling account's own key; each retry needs a fresh nonce.\n\nOwner consent uses recoverable secp256k1 (65-byte hex r || s || v; low-S, v 0/1 or 27/28). Sign Keccak256 of UTF-8 `{METHOD-uppercase}\\n{canonical-root-path}\\n{exact-query-without-?}\\n{hex-lowercase-SHA256(exact-body-bytes)}\\n{timestamp-ms}\\n{nonce}`. Both root and /api/v1 paths use the canonical root path. Do not reserialize a signed body. The deployment domain is signed in the body for POST and in the query for GET. HMAC/API-key, bearer-only and agent authority cannot authorize these operations. The server sets Cache-Control: no-store.","security":[{"ownerSignature":[]}],"parameters":[{"in":"header","name":"X-Timestamp","required":true,"schema":{"type":"string","pattern":"^[0-9]+$","maxLength":20},"description":"Unix milliseconds, within 30 seconds of server time."},{"in":"header","name":"X-Nonce","required":true,"schema":{"type":"string","pattern":"^[0-9]+$","maxLength":20},"description":"Unsigned 64-bit owner nonce. POST consumes a strictly increasing nonce durably even if later settlement fails; sign a fresh higher nonce when retrying the same client transfer id. GET consumes no nonce."}],"responses":{"200":{"description":"Committed result or authoritative read.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferEnrollmentResponse"}}}},"400":{"description":"INVALID_TRANSFER_REQUEST for invalid intent/domain/precision; TRANSFER_RISK_REJECTED for insufficient cash, open exposure, liquidation or system-loss constraints.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"401":{"description":"TRANSFER_SIGNATURE_REQUIRED: missing, forged, stale, agent or replayed POST owner proof. No additional payment was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"403":{"description":"TRANSFER_ACCOUNT_INELIGIBLE or TRANSFER_OUTFLOW_BLOCKED: ordinary enrollment or live source controls do not permit this request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"502":{"description":"TRANSFER_ENROLLMENT_OUTCOME_UNKNOWN: retry enrollment of the same account with a fresh owner signature and nonce.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}},"503":{"description":"TRANSFERS_UNAVAILABLE: disabled/unconfigured service, unavailable durable writer, invalid/expired control permit, pending committed fills or receipt capacity reached. No successful outcome is implied.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferError"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrdinaryTransferEnrollment"}}}},"x-ccxt-scope":"out-of-scope","x-ccxt-scope-reason":"Ordinary cross-owner payments use a recoverable owner signature and enrollment. CCXT transfer moves funds between balances of the same owner; no CCXT custody/signing adapter is shipped for this operation."}}},"components":{"securitySchemes":{"hmacAuth":{"type":"apiKey","in":"header","name":"X-API-Key","description":"HMAC API key authentication. Send three headers:\n- `X-API-Key`: your key ID (e.g. `nx_a1b2c3...`)\n- `X-Timestamp`: current time in unix milliseconds\n- `X-Signature`: `hex(hmac_sha256(secret, timestamp + \"\\n\" + METHOD + \"\\n\" + path + \"\\n\" + query + \"\\n\" + sha256_hex(body)))`\n\nTimestamp must be within 30 seconds of server time.\n\n**The key is network-scoped; the signature is not.** The canonical string above has no network component, so the same signed request is byte-identical on every host. What stops a testnet key from authenticating against mainnet is that the key does not exist in that host's key store — sign only for the host that minted the key, and never replay a signed payload against another network. Presented to the wrong host, a key is refused with the same opaque `401` as an unknown key and must not be retried there. See “API keys are bound to one network” in the API description."},"bearerAuth":{"type":"http","scheme":"bearer","description":"Session token from POST /auth/login. Used only for API key management (/keys endpoints). Session tokens are network-scoped exactly like API keys: a token from one network's `POST /auth/login` is not valid on another, so sign in and manage keys on the same host you intend to trade against."},"adminAuth":{"type":"http","scheme":"bearer","description":"Admin secret (ADMIN_SECRET environment variable). Used for tier management and service control."},"ownerSignature":{"type":"apiKey","in":"header","name":"X-Signature","description":"Recoverable secp256k1 owner signature over Keccak256 of the canonical request. This is not an HMAC API key or an agent signature. See each ordinary transfer operation for the complete canonical request contract."}},"responses":{"Unauthorized":{"description":"Authentication failed. All 401 responses return the same opaque body to prevent information leakage. A credential minted on a different network is one of the causes it hides — by design, so that a key id cannot be probed for existence on another network. Do not retry it against another host.","content":{"application/json":{"example":{"code":"unauthorized"}}}},"RateLimited":{"description":"Rate limit exceeded. **Retryable** — honour `retry-after` and back off; this is what separates a `429` from the permanent `403` jurisdiction refusals. Branch on the body's `code`, which is always `RATE_LIMIT_EXCEEDED`, and on `bucket` to learn WHICH budget bottlenecked — the buckets are independent, so a caller at its read ceiling can still submit orders and should not back off the whole client. Do not parse `message`: it is a diagnostic whose wording is not stable. Requests are charged by weight, so exhausting a budget takes fewer heavy calls than `x-ratelimit-limit` suggests. See “Rate limits” in the API description.","headers":{"X-RateLimit-Limit":{"schema":{"type":"integer"},"description":"Budget of request weight per second for your tier — not a count of requests. Sent on successful responses too."},"X-RateLimit-Remaining":{"schema":{"type":"integer"},"description":"Tokens left, in unit-cost requests: 10 means ten weight-1 requests or two weighing 5. Sent on successful responses too. Always `0` on this response."},"X-RateLimit-Reset":{"schema":{"type":"integer"},"description":"Unix timestamp (seconds) when the budget is whole again. Sent only on a `429`, never on a success."},"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying, never below 1. Derived from the **weighted** cost of the refused request, so it is not in the same unit as `X-RateLimit-Remaining`. Sent only on a `429`."},"X-RateLimit-Bucket":{"schema":{"type":"string","enum":["key","owner","order","cancel","ip","login"]},"description":"Which budget refused this request, mirroring the body's `bucket`. `key` — the per-API-key bucket; `owner` — the per-account request bucket; `order` — the trading-action bucket, order submission and amend only; `cancel` — the separate cancellation bucket, `DELETE` on the order surface only; `ip` — the public per-IP bucket for unauthenticated and gateway traffic; `login` — the dedicated per-IP bucket for `POST /auth/login`, tighter than `ip` and drawn from its own pool, so a login refusal leaves the read budget untouched and vice versa. The read, trading and cancellation budgets are counted separately, so a `429` on one does not mean the others are exhausted — in particular a `429` naming `order` never means your cancels are refused. Sent only on a `429`."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RateLimitError"},"examples":{"requestBudget":{"summary":"The per-owner request bucket","value":{"code":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded","tier":"Pro","bucket":"owner"}},"apiKeyBudget":{"summary":"The per-key bucket, which can bind before the owner bucket","value":{"code":"RATE_LIMIT_EXCEEDED","message":"API key rate limit exceeded","tier":"Pro","bucket":"key"}},"tradingBudget":{"summary":"The trading-action bucket — order submission and amend only","value":{"code":"RATE_LIMIT_EXCEEDED","message":"Order placement rate limit exceeded","tier":"Pro","bucket":"order"}},"cancellationBudget":{"summary":"The separate cancellation bucket — reached only by saturating cancels themselves","value":{"code":"RATE_LIMIT_EXCEEDED","message":"Order cancellation rate limit exceeded","tier":"Pro","bucket":"cancel"}},"ipBudget":{"summary":"The public per-IP bucket — unauthenticated and gateway traffic","value":{"code":"RATE_LIMIT_EXCEEDED","message":"IP rate limit exceeded","bucket":"ip","tier":"public"}}}}}},"BridgeBadRequest":{"description":"The request was invalid (e.g. unsupported chain or asset, amount below minimum).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeError"}}}},"BridgeNotFound":{"description":"No such resource is owned by the authenticated account (e.g. a deposit id that does not exist, or belongs to another account).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BridgeError"},"example":{"error":{"code":"deposit_not_found","message":"no deposit with that id belongs to this account"}}}}},"InvalidWindow":{"description":"The `window` query parameter was present but not one of `day`, `week`, `month`, or `all`. Returns a machine-readable error code.","content":{"application/json":{"example":{"code":"invalid_window"}}}},"InvalidCandleQuery":{"description":"The candle query was rejected before any data was read. Branch on `code`:\n\n| code | cause |\n|---|---|\n| `INVALID_TIMEFRAME` | `timeframe` is not one of the supported values |\n| `INVALID_TIME` | `startTime` or `endTime` was negative |\n| `INVALID_RANGE` | `startTime` was not strictly before `endTime` |\n\nEach is rejected rather than coerced. Serving a nearby timeframe, or silently swapping an inverted window, would return a confident answer to a question that was not asked. `message` is a human-readable diagnostic and its wording is not stable — do not match on it.","content":{"application/json":{"example":{"code":"INVALID_RANGE","message":"`startTime` (1776120300000) must be before `endTime` (1776033900000)"}}}},"AuthoritativeMarginUnavailable":{"description":"The engine-authoritative margin view is temporarily unavailable. Endpoints that derive balances from it (e.g. `withdrawable`) fail closed — returning this error rather than a locally-estimated, potentially unsafe figure. Transient; retry after a short delay.","content":{"application/json":{"example":{"code":"authoritative_margin_unavailable"}}}},"RestrictedJurisdiction":{"description":"Refused by a jurisdiction control. **Permanent for the caller's origin — do not retry.** Branch on the `code` (identical to the `x-nexus-block-reason` header): `US_RESTRICTED` for the US write restriction, `GEO_UNRESOLVED` when the origin could not be resolved and the write failed closed, or `RESTRICTED_JURISDICTION` for the sanctions list. Reads are never refused by the write restriction. See “Jurisdiction restrictions” in the API description.","headers":{"x-nexus-block-reason":{"$ref":"#/components/headers/XNexusBlockReason"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JurisdictionError"},"examples":{"usRestricted":{"summary":"US write restriction","value":{"code":"US_RESTRICTED","message":"This action is not available in the United States or to U.S. persons"}},"geoUnresolved":{"summary":"Origin could not be resolved; the write failed closed","value":{"code":"GEO_UNRESOLVED","message":"Unable to verify request origin; this action is unavailable"}},"restrictedJurisdiction":{"summary":"Sanctions list — can also be returned on a read","value":{"code":"RESTRICTED_JURISDICTION","message":"Access denied from restricted jurisdiction"}}}}}},"InvalidMarketId":{"description":"The supplied `market_id` path or query value is not a well-formed market identifier: it must match `^[A-Z0-9]+(-[A-Z0-9]+)*$` and be at most 64 characters. Branch on the body's `code`, which is always `INVALID_MARKET_ID`; `message` restates the rule and is a diagnostic whose wording is not stable. The result for a well-formed identifier that is not currently available is operation-specific and documented by that operation.","content":{"application/json":{"example":{"code":"INVALID_MARKET_ID","message":"market_id must match ^[A-Z0-9]+(-[A-Z0-9]+)*$ and be at most 64 bytes"}}}}},"parameters":{"NexusApiVersion":{"name":"X-Nexus-Api-Version","in":"header","required":false,"schema":{"type":"string","pattern":"^v\\d+\\.\\d+\\.\\d+$","maxLength":32},"example":"v0.7.0","description":"Released spec tag the client was compiled/pinned against (the client's `.api-version`), format `vMAJOR.MINOR.PATCH`. Every official client sends it on every request. Advisory and optional: the server accepts requests when it is absent, malformed, or names an unknown tag. A recognized tag older than the published minimum supported version may receive `426 Upgrade Required` (`api_version_unsupported`) per the API version-support policy. Excluded from the HMAC canonical string, so it is unauthenticated — never used for authentication, authorization, or access control; the version-support gate is a compatibility courtesy, not a security boundary, and spoofing the value only relaxes it. Consumers should treat the value as untrusted and bounded."},"UserAgent":{"name":"User-Agent","in":"header","required":false,"schema":{"type":"string","maxLength":256},"example":"nexus-exchange-rs/0.5.1","description":"Client identifier, format `nexus-exchange-<lang>/<version>` (e.g. `nexus-exchange-rs/0.5.1`). Advisory and optional; used by edge usage metering to segment traffic by client and version. Like all client-supplied headers it is unauthenticated and untrusted — never used for security decisions."},"MarketId":{"name":"market_id","in":"path","required":true,"schema":{"type":"string","pattern":"^[A-Z0-9]+(-[A-Z0-9]+)*$","maxLength":64},"example":"BTC-USDX-PERP","description":"Market identifier (e.g. BTC-USDX-PERP, ETH-USDX-PERP). Uppercase ASCII alphanumeric segments joined by single hyphens, at most 64 characters. A value outside that set is rejected with `400` (`INVALID_MARKET_ID`) before any store is read — it cannot name a market, so an empty result would assert that nothing traded rather than that the question was malformed. A well-formed identifier that names no listed market is a `404`, not a `400`."},"Cursor":{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque pagination cursor returned in the previous response's `X-Next-Cursor` header. Omit to fetch the first page. Treat the token as opaque: its format is not part of the contract and may change. Cursors do not expire. A malformed (unparseable) cursor is not an error — the server serves the first page. A well-formed cursor whose exact position has since been evicted from the retained window is not reset to the first page: pagination resumes at the nearest surviving boundary, so a resumed response is a continuation, not a fresh first page. A cursor is not bound to an operation or an account, so use it only with the operation that issued it. See “Cursor pagination” in the API description for how a walk terminates and what it does and does not guarantee."},"PortfolioWindow":{"name":"window","in":"query","required":false,"schema":{"allOf":[{"$ref":"#/components/schemas/PortfolioWindow"}],"default":"day"},"description":"Time window to return. Also selects the server-side downsample cadence and point capacity:\n\n| window | cadence | max points | span |\n|--------|---------|-----------|------|\n| `day`   | 5 min | 288 | 24 h |\n| `week`  | 1 h   | 168 | 7 d  |\n| `month` | 6 h   | 120 | 30 d |\n| `all`   | 1 d   | 366 | ~1 y |\n\nOmitted defaults to `day`. A value outside this set is rejected with `400` (`invalid_window`). If the parameter is repeated, the first value is used."},"CandleStartTime":{"name":"startTime","in":"query","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":1776033900000,"description":"Inclusive lower bound of the window, Unix milliseconds UTC, compared against each candle's bucket start. Omit for no lower bound.\n\n**Supplying `startTime` also sets the paging direction.** With it, the response begins AT this bound and runs forward, so a capped page is the OLDEST bars in the window and its last bar is where the next request resumes — `ccxt.fetchOHLCV`'s `since`, whose standard loop advances from the last bar it was handed. Without it, the response is the NEWEST bars in the window, which is what `fetchOHLCV` with no `since` means.\n\nThis is a correctness property, not a preference: served newest-first, a `since` deep in history would return the bars adjacent to now, the caller's cursor would jump to now, and the loop would exit after one page — reporting a complete history with everything in between missing. No error and no empty page, just a gap shaped like an answer.\n\nMust be strictly before `endTime` when both are given; an inverted window is rejected with `400` (`INVALID_RANGE`) rather than normalised, so a nonsensical window never reads back as an empty one. A negative value is rejected with `400` (`INVALID_TIME`).\n\nSpelled in camelCase rather than the snake_case used elsewhere in this API, deliberately: `ccxt.fetchOHLCV` and the Binance and Hyperliquid kline contracts all use these names."},"CandleEndTime":{"name":"endTime","in":"query","required":false,"schema":{"type":"integer","format":"int64","minimum":0},"example":1776120300000,"description":"Inclusive upper bound of the window, Unix milliseconds UTC, compared against each candle's bucket start. Omit to read up to now.\n\nWith no `startTime`, the operation returns the newest bars in the window, so a long history can be walked by moving `endTime` backwards. Prefer paging forward on `startTime` instead — that is the direction `ccxt.fetchOHLCV` pages in, and see that parameter for why. `endTime` alone does not change the direction.\n\nSee `startTime` for the rejection rules and for why both are camelCase."}},"schemas":{"LoginRequest":{"type":"object","description":"Wallet-signature sign-in payload. The message is a fixed string rather than a nonce-bearing challenge, so a captured signature stays valid: treat it as a bearer credential and never log or forward it. Signed with EIP-191 `personal_sign`, which is a different scheme from the EIP-712 typed signature `POST /agents/register` requires — the two are not interchangeable.","required":["message","signature"],"properties":{"message":{"type":"string","description":"Must be exactly: \"Sign in to Nexus Exchange\""},"signature":{"type":"string","description":"EIP-191 personal_sign hex (0x-prefixed, 65 bytes)"}}},"LoginResponse":{"type":"object","description":"A session, and the address it was recovered from.\n\n**The token is a full-authority wallet credential, not a key-management one.** It is presented as `Authorization: Bearer`, and the same middleware that accepts an API key's HMAC or an agent key accepts it — agent, HMAC and Bearer all resolve to one `AuthContext`, so the token reaches the order surface, the funds routes and credential management alike. Minting an API key is what it is normally used for, but it is not what it is limited to: treat it as equivalent to the wallet, and prefer a scoped agent key for anything long-lived.\n\n`address` is recovered from the signature rather than supplied by the caller, so it is the venue's statement of who signed, not an echo.","properties":{"token":{"type":"string","description":"Session token (64-char hex). Use as Bearer token for /keys endpoints."},"address":{"type":"string","description":"Recovered Ethereum address (0x-prefixed)"}}},"CreateKeyResponse":{"type":"object","description":"A newly minted HMAC credential. `secret` is served exactly once, here: `GET /keys` serves `KeyInfo`, which has no secret field at all, and no other operation can recover it. A client that discards this response has lost the key.","required":["key_id","secret"],"properties":{"key_id":{"type":"string","description":"Public key identifier: `nx_` followed by 16 lowercase hex characters. This is the value sent in the clear as the HMAC key id.","example":"nx_a1b2c3d4e5f67890"},"secret":{"type":"string","description":"HMAC signing secret: 64 lowercase hex characters (32 random bytes). Never served again. The operation's example elides it; it is not a real length."}}},"KeyInfo":{"type":"object","description":"An API key as `GET /keys` lists it. Carries no secret: the secret exists only in the `CreateKeyResponse` returned at creation.","required":["key_id","tier","label","created_at_ms"],"properties":{"key_id":{"type":"string","description":"Public key identifier, `nx_` + 16 lowercase hex characters.","example":"nx_a1b2c3d4e5f67890"},"tier":{"$ref":"#/components/schemas/Tier"},"label":{"type":["string","null"],"description":"Caller-supplied label from `POST /keys`, or `null` for a key created without one. **Nullable, not optional** — the field is always present, so read `null` rather than branching on a missing key."},"created_at_ms":{"allOf":[{"$ref":"#/components/schemas/TimestampMs"}],"description":"When the key was minted. The list is ordered by this value, oldest first."}}},"Tier":{"type":"string","enum":["Pro","MarketMaker","Unlimited"],"description":"Rate-limit tier of an API key or account override. Exactly three values, PascalCase on the wire.\n\n`Pro` is what any owner with no override resolves to. `MarketMaker` carries the market-making allowance. `Unlimited` is for gateway/proxy keys that multiplex many users and is exempt from per-key rate limiting — it is a value a client can really receive, so a two-value union over `Pro` and `MarketMaker` is wrong.\n\n`PUT /admin/tiers` ACCEPTS a laxer input vocabulary than this (`pro`, `market_maker`, `marketmaker` and `unlimited` all parse), but every response normalizes to the three values above."},"TierEntry":{"type":"object","description":"One account-level tier override.","required":["address","tier"],"properties":{"address":{"type":"string","description":"Account the override applies to. `GET /admin/tiers` always serves it canonically, as `0x` + 40 lowercase hex characters. `PUT /admin/tiers` echoes the request string back VERBATIM instead, so a caller that sent it unprefixed or mixed-case reads that form back — compare addresses case-insensitively and with `0x` stripped, never byte-for-byte.","example":"0x1234...abcd"},"tier":{"$ref":"#/components/schemas/Tier"}}},"WsTokenResponse":{"type":"object","description":"A minted WebSocket token and its expiry, from `POST /ws/token`.","required":["token","expires_at"],"properties":{"token":{"type":"string","description":"Single-use token: 48 lowercase hex characters (24 random bytes). Pass it as `?token=TOKEN` when upgrading to `GET /ws`."},"expires_at":{"allOf":[{"$ref":"#/components/schemas/TimestampMs"}],"description":"When the token stops being accepted — mint time plus the 60 s TTL. Read it rather than re-deriving the deadline from your own clock."}}},"WsTokenLegacyResponse":{"type":"object","description":"A minted WebSocket token from the legacy `POST /ws-tokens`. Deliberately narrower than `WsTokenResponse`: this endpoint discards the expiry the token store computes, so there is none to serve and the 60 s TTL has to be assumed. One more reason to prefer `POST /ws/token`.","required":["token"],"properties":{"token":{"type":"string","description":"Single-use token: 48 lowercase hex characters (24 random bytes)."}}},"Market":{"type":"object","description":"A market's static definition and risk parameters: the trading pair, its tick and lot granularity, order-size bounds, and the margin rates and caps the engine enforces against it. These are configuration rather than live state — nothing here moves with the book. For prices, volume and lifecycle status see `MarketSummary`; for the live book see `OrderBook`.\n\n`lot_size` is order granularity and is **not** a contract multiplier: `contractSize` is the separate field carrying the base-asset quantity one contract represents.\n\nENG-13528 moved four properties onto CCXT's `Market` vocabulary: `market_id` → `id`, `base_asset` → `base`, `quote_asset` → `quote`, `contract_size` → `contractSize`. The rest are Nexus extensions CCXT has no counterpart for and keep our names. `MarketSummary` and `Position` still spell the multiplier `contract_size`; `Position` belongs to ENG-13312, so the surfaces differ deliberately.","properties":{"id":{"type":"string","description":"Market identifier. Named `market_id` until ENG-13528 renamed it to CCXT's `id`."},"base":{"type":"string","description":"Base asset. Named `base_asset` until ENG-13528 renamed it to CCXT's `base`."},"quote":{"type":"string","description":"Quote asset. Named `quote_asset` until ENG-13528 renamed it to CCXT's `quote`. This is also the asset the market settles in today, but `settle` is deliberately not derived from it — a market quoted in one asset and settled in another would make that derivation wrong, and the venue has yet to state settlement per market."},"tick_size":{"$ref":"#/components/schemas/Decimal"},"lot_size":{"$ref":"#/components/schemas/Decimal"},"min_order_size":{"$ref":"#/components/schemas/Decimal"},"max_order_size":{"$ref":"#/components/schemas/Decimal"},"initial_margin_rate":{"$ref":"#/components/schemas/Decimal"},"maintenance_margin_rate":{"$ref":"#/components/schemas/Decimal"},"max_leverage":{"type":"integer"},"max_open_interest_notional":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Static one-sided open-interest cap in quote notional; `null` when the market sets none. Mutable on a running venue. This is the value an operator sets and changes; the bound an order is actually refused against is the minimum of this and the market's dynamic underlying terms, which can only tighten it. The two coincide on every market listed today. Distinct from `max_open_interest`, an independent cap denominated in base quantity that this endpoint also returns but this schema does not yet declare — they are two separate bounds, not two spellings of one."},"price_band_bps":{"type":"integer","description":"Order-vs-mark price collar, in basis points either side of the mark. A limit price further from the mark than this is refused at admission; liquidation orders are exempt. Mutable on a running venue. Always present and always in (0, 5000] - a market with no collar is not expressible."},"contractSize":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Base-asset quantity one contract represents, as a decimal string. Named `contract_size` until ENG-13528 renamed it to CCXT's `contractSize`. `\"1\"` on every market listed today — every market is linear, and the engine's own notional math (`size × price`, with no multiplier term) is only consistent with `1`. Published rather than assumed so a client reading a position's contract count does not have to hardcode the venue's answer.\n\n**Not `lot_size`.** That is order granularity — the increment a size must be a multiple of. This is the multiplier. Conflating them misreports position size on any market where they differ."}}},"MarketSummary":{"type":"object","description":"A market's live trading state: last trade price, traded volume, trade count, and lifecycle status with its halt detail.\n\nTwo fields are easy to misread and say so themselves. `last_trade_price` is what the market last traded at, **not** the mark the engine derives for margin and liquidations. And `volume_24h` is not a 24-hour window despite the name — read that field's own description before charting it.","properties":{"market_id":{"type":"string"},"last_trade_price":{"type":["number","null"],"description":"Last trade price (\"what the market is trading at\"). NOT the mark; the engine-derived mark is exposed separately."},"volume_24h":{"type":"number","description":"Traded notional in quote (USDX). **This is not a 24h window.** `MarketSummary.volume_24h` and `Ticker.quoteVolume` are the SAME accumulator under a misleading name: nothing decays either of them. Read `GET /stats/volume` when you want the cumulative figure stated honestly, with the coverage start that says what it is cumulative since. This field is a JSON number produced by a lossy `f64` conversion; `/stats/volume` carries the same quantity as an exact decimal string."},"trade_count":{"type":"integer"},"status":{"type":"string","enum":["listed","active","halted","restricted","reduce_only","settling","delisted"],"description":"Lifecycle state; only 'active'/'halted' produced today (full vocabulary declared in v0.9.0 / ENG-10445)"},"halt_reason":{"type":["string","null"]},"halted_at":{"type":["integer","null"],"format":"int64","description":"Unix ms timestamp when market was halted"},"adl_event_count":{"type":"integer","description":"Cumulative ADL settlement events for this market"}}},"MarketStatus":{"type":"object","description":"Per-market lifecycle status. Sized for the full seven-state lifecycle vocabulary (v0.9.0 / ENG-10445); only 'active' and 'halted' are produced today (v0.21 halt surface).","properties":{"market_id":{"type":"string"},"status":{"type":"string","enum":["listed","active","halted","restricted","reduce_only","settling","delisted"]},"halt_reason":{"type":["string","null"]},"halted_at":{"type":["integer","null"],"format":"int64"},"adl_event_count":{"type":"integer"}}},"MarketRiskParams":{"type":"object","description":"Per-market risk parameters: margin rates and maximum leverage.","properties":{"market_id":{"type":"string"},"max_leverage":{"type":"integer","description":"Maximum leverage allowed for this market"},"initial_margin_rate":{"$ref":"#/components/schemas/Decimal","description":"Initial margin requirement as a decimal ratio (e.g., 0.05 = 5%)"},"maintenance_margin_rate":{"$ref":"#/components/schemas/Decimal","description":"Maintenance margin requirement as a decimal ratio (e.g., 0.025 = 2.5%)"}}},"MarkPriceResponse":{"type":"object","description":"The current mark price for one market. `/markets/{market_id}/mark-price` and its `/api/v1` twin are the same handler behind two mounts, so the two responses cannot diverge.","required":["market_id","mark_price"],"properties":{"market_id":{"type":"string","description":"The market the price is for, echoed from the path.","example":"BTC-USDX-PERP"},"mark_price":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Mark price as a decimal string at the market's own scale — trailing zeros are significant and preserved (`\"50011.60\"`, not `\"50011.6\"`). Never a zero or a null: an absent or stale mark is signalled by the response status, not by a sentinel in this field.","example":"50011.60"}}},"AdlClosureRecord":{"type":"object","description":"One counterparty's forced closure within an ADL settlement.","properties":{"account_id":{"type":"string","description":"0x-prefixed address of the counterparty whose position was closed"},"position_closed":{"$ref":"#/components/schemas/Decimal","description":"Decimal quantity closed"},"settlement_amount":{"$ref":"#/components/schemas/Decimal","description":"Decimal amount charged to the counterparty"}}},"AdlEventRecord":{"type":"object","description":"Single ADL settlement (insurance fund depleted → counterparty closures). v0.21.","properties":{"market_id":{"type":"string"},"target_account":{"type":"string","description":"0x-prefixed bankrupt account"},"bankruptcy_price":{"$ref":"#/components/schemas/Decimal"},"bad_debt_absorbed_by_fund":{"$ref":"#/components/schemas/Decimal"},"counterparty_closures":{"type":"array","items":{"$ref":"#/components/schemas/AdlClosureRecord"}},"sequence":{"type":"integer","description":"Engine event sequence number"},"timestamp":{"$ref":"#/components/schemas/TimestampMs","description":"Unix ms"}}},"RateLimitError":{"type":"object","description":"The body of a 429. Branch on `code` (always `RATE_LIMIT_EXCEEDED`) and on `bucket` to learn WHICH budget bottlenecked. Schematised rather than left to `examples` because `bucket` is meant to be read by machines: an example gives a generated client no field and a validator nothing to check `\"bucket\": \"ownr\"` against. `bucket` here and the `X-RateLimit-Bucket` header carry the same value and the same enum.","required":["code","message","bucket"],"properties":{"code":{"type":"string","enum":["RATE_LIMIT_EXCEEDED"],"description":"Always `RATE_LIMIT_EXCEEDED` on this response. The other 429s this API can return carry different codes (`too_many_agents`, `rate_limited`, `cap_exceeded`, `daily_limit_exceeded`) and are not this shape."},"message":{"type":"string","description":"A diagnostic whose wording is NOT stable. Do not parse it; branch on `bucket`."},"bucket":{"type":"string","enum":["key","owner","order","cancel","ip","login"],"description":"Which budget refused this request. `key` — the per-API-key bucket; `owner` — the per-account request bucket; `order` — the trading-action bucket, order submission and amend only; `cancel` — the separate cancellation bucket, `DELETE` on the order surface only; `ip` — the public per-IP bucket for unauthenticated and gateway traffic; `login` — the dedicated per-IP bucket for `POST /auth/login`, tighter than `ip` and drawn from its own pool, so a login refusal leaves the read budget untouched and vice versa. The buckets are independent, so a caller at its read ceiling can still submit orders, and a caller at its submission ceiling can still cancel — do not back off the whole client on any one of them."},"tier":{"type":"string","description":"The rate-limit tier the refused request was charged against — `public` for the per-IP bucket, otherwise the authenticated key's tier. Absent on responses from paths that refuse before a tier is resolved."}}},"Ticker":{"type":"object","description":"CCXT-compatible ticker with 24h statistics","properties":{"symbol":{"type":"string"},"timestamp":{"$ref":"#/components/schemas/TimestampMs","description":"Unix ms"},"datetime":{"type":"string","format":"date-time"},"high":{"type":["number","null"]},"low":{"type":["number","null"]},"bid":{"type":["number","null"]},"bidVolume":{"type":["number","null"]},"ask":{"type":["number","null"]},"askVolume":{"type":["number","null"]},"open":{"type":["number","null"]},"close":{"type":["number","null"]},"last":{"type":["number","null"]},"change":{"type":["number","null"]},"percentage":{"type":["number","null"]},"baseVolume":{"type":["number","null"]},"quoteVolume":{"type":["number","null"],"description":"Traded notional in quote (USDX). **This is not a 24h window.** `MarketSummary.volume_24h` and `Ticker.quoteVolume` are the SAME accumulator under a misleading name: nothing decays either of them. Read `GET /stats/volume` when you want the cumulative figure stated honestly, with the coverage start that says what it is cumulative since. Same accumulator, and the same lossy `f64` conversion, as `MarketSummary.volume_24h`."},"markPrice":{"type":["number","null"],"description":"Engine-derived mark price (oracle + premium-index), falling back to the last trade until the first mark-price poll lands. The raw last trade is carried by `last`."},"indexPrice":{"type":["number","null"]},"info":{"type":"object"}}},"OrderBook":{"type":"object","description":"CCXT-compatible order book. Bids/asks are [price, amount] arrays.","properties":{"symbol":{"type":"string"},"bids":{"type":"array","items":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}]}},"asks":{"type":"array","items":{"type":"array","prefixItems":[{"type":"number"},{"type":"number"}]}},"timestamp":{"$ref":"#/components/schemas/TimestampMs"},"datetime":{"type":"string","format":"date-time"},"nonce":{"type":"integer","format":"int64"}}},"Trade":{"type":"object","description":"CCXT-compatible trade record","properties":{"id":{"type":"string","format":"uuid"},"symbol":{"type":"string"},"price":{"type":"number"},"amount":{"type":"number"},"cost":{"type":"number"},"side":{"type":"string","enum":["buy","sell"]},"timestamp":{"$ref":"#/components/schemas/TimestampMs"},"datetime":{"type":"string","format":"date-time"},"takerOrMaker":{"type":["string","null"]},"is_liquidation":{"type":"boolean"},"info":{"type":"object"}}},"FundingPremiumSample":{"type":"object","description":"One premium-index observation between settlements, as served by `/markets/{market_id}/funding-samples`.\n\nCarries the premium and its timestamp, and nothing else. The settled funding rate, mark price and oracle price are properties of a settled *window*, not of an intra-window sample, and the event these samples are folded from does not carry them. Read `/markets/{market_id}/funding` for those — it returns `FundingSample`.","required":["timestamp","premium_index"],"properties":{"timestamp":{"$ref":"#/components/schemas/TimestampMs"},"premium_index":{"$ref":"#/components/schemas/Decimal","description":"`(trade_reference_price - oracle_price) / oracle_price` at the sample instant — the perpetual's own traded reference against the index, not the mark price.\n\nReads `0` until the market has traded: with no trade reference available the value falls back to the oracle price, which makes the numerator exactly zero. A long run of `\"0\"` samples means the market has not traded, not that the perpetual is at parity with spot."}}},"FundingSample":{"type":"object","description":"A settled funding window, as served by `/markets/{market_id}/funding`: the realized rate for the window, plus the premium and prices of the last observation folded into it.\n\nThis is not the shape of `/markets/{market_id}/funding-samples` — see `FundingPremiumSample`.","properties":{"timestamp":{"$ref":"#/components/schemas/TimestampMs"},"fundingRate":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"The realized funding rate for the settled window. Named `funding_rate` until ENG-13528 renamed it to CCXT's `fundingRate`, which is what `fetchFundingRateHistory` returns."},"premium_index":{"$ref":"#/components/schemas/Decimal"},"mark_price":{"$ref":"#/components/schemas/Decimal","description":"Named `mark_price` for wire compatibility only — this is not the blended mark price. It carries the perp reference: the volume-weighted median of the market's own recent trades at the settled window's last funding sample, which is the value the premium is measured against. The mark that margin and liquidations use is a blend of the oracle price and that trade reference, weighted heavily toward the oracle by default, so the two are not interchangeable — read the real mark from `GET /markets/{market_id}/mark-price`.\n\nThis is that single last sample, not an average across the window; `funding_rate` beside it is the time-weighted result. Reads the oracle price until the market has traded, which makes `premium_index` exactly zero, and `\"0\"` when the window settled with no oracle sample at all."},"oracle_price":{"$ref":"#/components/schemas/Decimal"}}},"OrderRequest":{"type":"object","description":"Order placement request. Supports plain `Limit` / `Market` orders and six conditional order types (`StopLimit`, `StopMarket`, `TakeProfitLimit`, `TakeProfitMarket`, `TrailingStop`, `TrailingLimit`). Field requirements depend on `order_type`:\n\n- **Limit-family** (`Limit`, `StopLimit`, `TakeProfitLimit`) require a limit `price`.\n- **Triggerable, non-trailing** orders (`StopLimit`, `StopMarket`, `TakeProfitLimit`, `TakeProfitMarket`) require a `trigger_price` (the legacy `stop_price` field is accepted as a fallback when `trigger_price` is absent).\n- **`TrailingStop`** is market-only — it fires as a market order — and requires `trailing_offset_bps`. It does not take a limit `price` or a `trigger_price` (the trigger anchor is derived from the mark price and the offset).\n- **`TrailingLimit`** trails like `TrailingStop` but fires a limit order instead of a market order. It requires both `trailing_offset_bps` (the trailing trigger) and `limit_offset_bps` (the fire-time limit offset). It does not take `price`, `trigger_price`, or `stop_price`; the limit price is computed at fire time from the mark that crossed the offset.","required":["market_id","side","order_type","quantity","time_in_force"],"properties":{"market_id":{"type":"string"},"side":{"type":"string","enum":["Buy","Sell"]},"order_type":{"type":"string","description":"Order type. `Limit` and `Market` are unconditional. The remaining six are conditional: `StopLimit` / `StopMarket` fire when the mark price crosses `trigger_price` in the adverse direction; `TakeProfitLimit` / `TakeProfitMarket` fire on the favorable direction; `TrailingStop` fires as a market order when the mark retraces from its best-seen extreme by `trailing_offset_bps`; `TrailingLimit` fires the same way but rests a limit order priced off the fire price by `limit_offset_bps`. See the schema description for per-type field requirements.","enum":["Limit","Market","StopLimit","StopMarket","TakeProfitLimit","TakeProfitMarket","TrailingStop","TrailingLimit"]},"price":{"$ref":"#/components/schemas/Decimal","description":"Limit price. Required for limit-family orders (`Limit`, `StopLimit`, `TakeProfitLimit`); omit for market-family and trailing orders."},"quantity":{"$ref":"#/components/schemas/Decimal"},"time_in_force":{"type":"string","description":"Time-in-force policy. `PostOnly` rejects the order if it would take liquidity (cross the book) on entry, guaranteeing it rests as a maker.","enum":["GTC","IOC","FOK","PostOnly"]},"reduce_only":{"type":"boolean"},"stop_price":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"deprecated":true,"description":"**Deprecated** — use `trigger_price` instead. Legacy trigger threshold for the stop / take-profit family. Accepted as a fallback only when `trigger_price` is absent; when both are supplied, `trigger_price` wins. Ignored for `Limit`, `Market`, `TrailingStop`, and `TrailingLimit` orders."},"trigger_price":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Canonical trigger threshold for triggerable, non-trailing orders (`StopLimit`, `StopMarket`, `TakeProfitLimit`, `TakeProfitMarket`), which require it (the legacy `stop_price` field is accepted as a fallback when this is omitted). Not used by `Limit`, `Market`, `TrailingStop`, or `TrailingLimit` orders."},"trailing_offset_bps":{"type":["integer","null"],"minimum":0,"description":"Trailing offset in basis points (1 bp = 0.01%). Required for `TrailingStop` and `TrailingLimit` orders; ignored for all other order types. The trailing trigger fires once the mark price retraces from its best-seen extreme by this many basis points: `TrailingStop` fires a market order, `TrailingLimit` fires a limit order priced by `limit_offset_bps`. A value of `0` is accepted and fires the trigger at the first mark-price evaluation after placement (no retracement required)."},"limit_offset_bps":{"type":["integer","null"],"minimum":0,"maximum":9999,"description":"Offset in basis points for the fired limit price (`TrailingLimit` only; required together with `trailing_offset_bps`). When the trailing trigger fires at `fire_price`, the injected limit order rests at `fire_price` * (1 + offset) for buys / * (1 - offset) for sells, tick-rounded toward the tighter bound. A value of 0 rests the limit exactly at `fire_price`. Ignored for other order types."},"stp":{"type":["string","null"],"enum":["CancelNewest","CancelOldest","DecrementAndCancel",null],"description":"Opt-in self-trade prevention. Omit the field, or send `null`, to allow self-matching — that is the default and the industry-standard behaviour, and the engine will fill your order against your own resting order. Set a mode to have the engine intervene when a taker meets a maker on the same account instead.\n\n`CancelNewest` cancels the incoming taker and leaves the maker resting; the taker stops walking the book entirely, so remaining taker size beyond that maker is cancelled too. `CancelOldest` cancels the resting maker and lets the taker carry on against other accounts' makers. `DecrementAndCancel` reduces both sides by `min(taker_remaining, maker_size)` and cancels the smaller side, leaving the larger to continue at the reduced quantity.\n\nThe check is applied **per encountered same-account maker**, not once at order entry, so a taker crossing several of your own makers is evaluated at each one. It sits in the shared matching path, so it applies to every order type rather than a subset. When a mode cancels an order, that order comes back with `cancellation_reason` set to the object `{\"Stp\": \"<mode>\"}` — see `Order.cancellation_reason`."},"client_id":{"type":["string","null"],"maxLength":128,"description":"Client-supplied idempotency key for this order, opaque to the venue and scoped to your account: two accounts may use the same value independently.\n\n**Retrying a submit with a `client_id` you have already used does not place a second order.** A client cannot tell \"the venue never received my order\" from \"it received it, accepted it, and the response was lost\" — both look like a timeout — so a retry is the only safe response to one, and this is what makes retrying safe. The repeat is answered `200` with the order the first request created, rather than `201` with a new one. Submit the retry byte-identically; the venue answers with the order it already holds and does not compare the rest of the body against it.\n\n**Retention is bounded.** The venue remembers a limited number of recent keys per account, sized to cover a retry window rather than a session. A key reused long after that window is treated as new, so it is not a durable uniqueness constraint and must not be used as one — do not rely on it to prevent a duplicate hours later.\n\nAn amend (`PATCH /orders/{order_id}`) carries the key to the replacement order, so a retry after an amend is answered with the replacement. Omit the field, or send `null`, to opt out; an empty string is treated as omitted.\n\n`POST /orders/batch` honours the key per entry, so a retried batch can mix replays of orders you already hold with genuinely new ones: a replayed entry comes back as a normal success row carrying the original order, subject to the same resting-only limit as above. Two entries in one batch sharing a key accept the first and refuse the second."},"max_slippage_bps":{"type":["integer","null"],"minimum":0,"description":"Server-enforced slippage cap in basis points (1 bp = 0.01%). Omit the field for no cap. When set, the engine captures the book mid-price `(best_bid + best_ask) / 2` once at submission and requires the order's running fill VWAP to stay inside `mid ± mid × bps / 10000` as it walks the book. Fills made before the cap binds stand; the fill that would push the VWAP outside the band is not made and the unfilled remainder is cancelled — the order comes back on the normal `201` with `status` `Cancelled` and `cancellation_reason` `SlippageCap`, not as an error. Because the reference is the mid captured server-side at submission and the bound is on the running VWAP rather than on the worst individual fill, this is a stricter guarantee than a marketable limit price derived client-side from a previously sampled reference price.\n\nApplies to the market family: `Market`, plus `StopMarket`, `TakeProfitMarket`, and `TrailingStop`, which carry the field through their conversion and have the cap applied when they fire as market orders. Accepted but ignored on `Limit`, `StopLimit`, `TakeProfitLimit`, and `TrailingLimit` — a limit order already fills at its limit price or better.\n\nTwo edges worth knowing. A mid-price requires **both** sides of the book to be non-empty, so a capped order submitted while either side is empty is rejected with `InsufficientLiquidity` (a `400` on `POST /orders`, an `err` item on `POST /orders/batch`) — including when the empty side is the order's own and the order could otherwise have filled. And `0` does not mean \"no cap\": it collapses the band onto the mid exactly, so against any book with a non-zero spread the order cancels with zero fills. `POST /orders/preview` accepts the field but does not apply it — `expected_fill_vwap` always walks the full book."}}},"AmendOrderRequest":{"type":"object","description":"Atomic cancel-replace amend of a resting order. At least one of `price` (new limit price) or `size` (new quantity) must be present; an empty body is rejected with InvalidAmend. `size` is the new TOTAL quantity, including whatever the order has already filled, and must be greater than its `filled` — an amend cannot take back an execution, so a size at or below it is rejected with InvalidAmend.","minProperties":1,"properties":{"price":{"$ref":"#/components/schemas/Decimal"},"size":{"$ref":"#/components/schemas/Decimal"}}},"OrderResponse":{"type":"object","description":"What `POST /orders` returns: the accepted order and the executions that placement produced immediately.\n\n**`fills` is only populated here.** Fetching the same order afterwards returns a bare `Order`, which carries the aggregates but not the individual executions — so a client that discards this response cannot recover the fill detail from the order endpoints and must read `/fills` instead. Persist it at placement if you need it.","properties":{"order":{"$ref":"#/components/schemas/Order"},"fills":{"type":"array","description":"The executions this placement produced, straight off the matching engine. These are `ExecutionFill`, NOT the account-projected `Fill` that `GET /fills` returns — different fields, and a `side` that means something different. See `ExecutionFill`.","items":{"$ref":"#/components/schemas/ExecutionFill"}}}},"OrderResult":{"description":"One entry in the array returned by POST /orders/batch. The batch is sequential and non-atomic, so each entry independently reports either a placed order or a per-order rejection, in request order. Internally tagged by `outcome`: `ok` carries the same `{ order, fills }` shape as POST /orders, `err` carries the same `{ error, message }` shape as the global error envelope.","oneOf":[{"$ref":"#/components/schemas/OrderResultOk"},{"$ref":"#/components/schemas/OrderResultErr"}],"discriminator":{"propertyName":"outcome","mapping":{"ok":"#/components/schemas/OrderResultOk","err":"#/components/schemas/OrderResultErr"}}},"OrderResultOk":{"type":"object","description":"A placed order in a batch result (outcome `ok`).","required":["outcome","order"],"properties":{"outcome":{"type":"string","enum":["ok"]},"order":{"$ref":"#/components/schemas/Order"},"fills":{"type":"array","description":"Same engine-side executions as `OrderResponse.fills`.","items":{"$ref":"#/components/schemas/ExecutionFill"}}}},"OrderResultErr":{"type":"object","description":"A rejected order in a batch result (outcome `err`). Mirrors the global error envelope.","required":["outcome","error","message"],"properties":{"outcome":{"type":"string","enum":["err"]},"error":{"type":"string","description":"Machine-readable error code."},"message":{"type":"string","description":"Human-readable error message."}}},"Order":{"type":"object","description":"An order and its lifecycle, as the read endpoints return it. Every placement option is echoed back, so this schema is also the record of how an order was placed: `postOnly`, `reduceOnly`, `stp`, `max_slippage_bps`, `triggerPrice`, the trailing anchor and offset, and the `clientOrderId` the caller supplied.\n\nField names follow CCXT's unified vocabulary wherever CCXT defines one and stay `snake_case` for the Nexus extensions it does not — the same rule `Trade` follows (ENG-13314). Money remains a decimal string rather than becoming a JSON number (EDR-012), so unlike `Trade` this schema pairs camelCase keys with decimal-string values. `POST /orders` still accepts the request vocabulary (`market_id`, `quantity`, `client_id`) and returns this one — the asymmetry is deliberate and is ENG-13524's to settle.\n\nQuantities are three figures: `amount` is the total the order was placed for, `filled` what has executed, and `remaining` their difference — served by the venue rather than left to the caller, because these are decimal strings and the subtraction is where a client loses precision (ENG-13272). All three survive an amend, which replaces the order rather than mutating it.\n\nThe four fill aggregates — `average`, `cost`, `fee` and `lastTradeTimestamp` — are `null` together when the venue cannot compute them, with `fill_totals_error` carrying the reason. They are not per-fill detail: the individual executions are on `/fills`, and on `OrderResponse` at placement time.","properties":{"id":{"type":"string","format":"uuid"},"symbol":{"type":"string"},"account_id":{"type":"string"},"side":{"type":"string","enum":["Buy","Sell"]},"type":{"type":"string"},"limit_offset_bps":{"type":["integer","null"],"description":"Fire-time limit offset in basis points, echoed for `TrailingLimit` orders (see the `OrderRequest.limit_offset_bps` placement field); null for other order types."},"stp":{"type":["string","null"],"description":"The self-trade prevention mode the order was placed with, echoed back (see the `OrderRequest.stp` placement field for what each mode does). `null` for an order placed without one, which is the default and means self-matching was allowed. One of `CancelNewest`, `CancelOldest` or `DecrementAndCancel`.\n\nDeliberately not a closed enum here, for the same reason as `cancellation_reason`: the mode set has changed before (D-026 supersedes D-014) and a mode added later must not break a client pinned to an older spec tag. `OrderRequest.stp` does enumerate the modes, because there the value is one you supply and the server validates it."},"max_slippage_bps":{"type":["integer","null"],"minimum":0,"description":"Slippage cap in basis points, echoed for orders placed with one (see the `OrderRequest.max_slippage_bps` placement field); null for orders placed without a cap."},"price":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The order's limit price, or null for an order type that has none. CCXT defines `price` as the limit price, so ENG-13314 renamed `limit_price` to it.\n\nThis is NOT the `price` that ENG-6776 removed from this schema. That one was a phantom — documented, and emitted by neither producer, so a client branching on it got `undefined`. This property is the value the response has carried all along, now under CCXT's name for it."},"stopPrice":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Stop price for a stop order, null otherwise."},"triggerPrice":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The price that armed a triggered order, null for order types that are not triggered."},"stopLossPrice":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The trigger price of a stop-loss order — CCXT's `stopLossPrice`. Set only on the stop family (`StopLimit`, `StopMarket`), where it restates the order's own `triggerPrice` under CCXT's name; null on every other order type.\n\nNexus models a bracket's stop loss as its own reduce-only child order, and records no parent→child link, so this is an order stating its OWN threshold — which is also CCXT's definition of the field. A parent order does not report its bracket's price here.\n\nTrailing types are deliberately null: their threshold is `trailing_anchor` offset by `trailing_offset_bps`, not a fixed price, so a value here would state a number the engine does not use.\n\nDOES NOT SURVIVE THE FIRE. This field is keyed on `type`, and the venue rewrites `type` when the trigger fires (`StopMarket` becomes `Market`, `StopLimit` becomes `Limit`) while retaining `triggerPrice`. The same order therefore reports a price here while it is resting and `null` once it has fired, with `triggerPrice` unchanged throughout. Reconcile a stop across its whole life on `triggerPrice`, not on this field."},"takeProfitPrice":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The trigger price of a take-profit order — CCXT's `takeProfitPrice`. Set only on the take-profit family (`TakeProfitLimit`, `TakeProfitMarket`), where it restates the order's own `triggerPrice` under CCXT's name; null on every other order type.\n\nThe mirror of `stopLossPrice`, and null under the same conditions — see that field for why a bracket parent does not report its child's price, why trailing types state nothing, and why a FIRED take profit reports null here while keeping its `triggerPrice`."},"trailing_anchor":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The extreme price a trailing order is measured from, null for non-trailing orders."},"trailing_offset_bps":{"type":["integer","null"],"minimum":0,"description":"Trailing distance in basis points for a trailing order; null otherwise. Echoed from placement, like `max_slippage_bps`."},"reduceOnly":{"type":"boolean","description":"Whether the order may only reduce an existing position, echoed from placement (`OrderRequest.reduce_only`).\n\nRead this rather than inferring it. The field was in every `GET /orders` response before ENG-6776 documented it — the indexer serializes the shared `Order` type verbatim — but absent from this schema, so a client needing it was guessing from order type and position side instead of reading the flag the venue recorded."},"postOnly":{"type":"boolean","description":"Whether the order was placed post-only: cancelled rather than executed if it would have taken liquidity."},"is_liquidation":{"type":"boolean","description":"Whether the liquidation engine created this order rather than the account. Liquidation fills are fee-exempt on both legs (ADR-0004), which is why a `Fill.fee` of \"0\" against such an order is a real zero and not an unstated fee."},"clientOrderId":{"type":["string","null"],"description":"The client-supplied identifier for the order, echoed back; null when placement supplied none."},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Total quantity the order was placed for, fills included. What is still executable is `remaining`, which the venue serves rather than making the caller subtract. An amend preserves it unless the amend supplies a new `size`."},"filled":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Quantity executed so far, out of `amount`. Carried across an amend, so on a replacement it still counts every fill of the order it descends from."},"remaining":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Quantity not yet executed — `amount - filled` — stated by the venue rather than re-derived by the caller. CCXT's unified `remaining`.\n\nDerivable, and served natively anyway (ENG-13272). Quantities here are decimal strings (EDR-012), so a caller subtracting them in JavaScript converts through a float first and can land a value that is wrong in the last places — which is what consumers do today. The engine already holds this figure exactly, so the venue reports it and the subtraction happens once, in decimal, rather than once per client.\n\nIt is the arithmetic difference at all times, NOT an assertion that the quantity is still working. A cancelled, expired or rejected order reports the quantity that never executed, not a quantity still on the book; `status` is what distinguishes those, and a client sizing a replacement order should read both. `0` on a fully filled order.\n\nCarried across an amend on the same terms as its two operands: `PATCH /orders/{order_id}` preserves `amount` unless the amend supplies a new `size`, and carries `filled` across, so the difference stays consistent with the pair on the same response. Unlike the four fill aggregates it is never `null` and has no `fill_totals_error` case — it needs no fill history to compute, only the order's own two quantities."},"status":{"type":"string","description":"Lifecycle state. `Triggered` is a transient engine state stamped on a stop/take-profit/trailing order at the moment it fires, before the resulting order is submitted; no client-facing response is expected to carry it, because every submit outcome overwrites it (`Open`/`PartiallyFilled` when the converted order rests, `Filled`, `Cancelled`, or `Rejected`). It is listed because the wire type can hold it, so a consumer that rejects unknown values would fail on it rather than degrade. Treat this set as growable and prefer tolerating an unrecognised status to hard-failing.","enum":["Open","PartiallyFilled","Filled","Cancelled","Expired","Rejected","Triggered"]},"cancellation_reason":{"oneOf":[{"type":"null","description":"The order has not reached a terminal status, or no cause was recorded."},{"type":"string","description":"Any cause other than self-trade prevention, as a bare string."},{"type":"object","description":"A cause that carries a payload. Exactly one key, naming the cause. `additionalProperties` is deliberately not closed, so a cause added later does not invalidate this schema.","minProperties":1,"maxProperties":1,"properties":{"Stp":{"type":"string","description":"Self-trade prevention cancelled the order; the value is the STP mode that fired (`CancelNewest`, `CancelOldest` or `DecrementAndCancel`)."}}}],"description":"Why the order reached a terminal `Cancelled` or `Rejected` status. `null` for every other status, and for a terminal order the engine recorded no cause for. The key is always present.\n\n**Two wire shapes.** The engine's reason type is an externally tagged enum, so every cause except self-trade prevention is a bare string, while self-trade prevention is a single-key object naming the mode that fired: `{\"Stp\": \"CancelNewest\"}`. Branch on the JSON type before reading the value.\n\nThe string form is one of `User` (an explicit cancel or cancel-all), `SlippageCap` (a market order's running fill VWAP left the `max_slippage_bps` band — see that field), `Liquidation` (cancelled ahead of a liquidation, or the unfilled remainder of one), `Expired` (an IOC, FOK or market remainder that cannot rest on the book), `MarketHalt`, `AmendReplace` (carried on the *original* order of an atomic cancel-replace via `PATCH /orders/{order_id}`), `InsufficientLiquidity` (a stop, stop-limit or trailing stop fired into an empty opposite side), `BracketClosed` or `BracketFlipped` (a bracket child whose parent position closed to zero or flipped sign), or `PriceBandExceeded` (the order-vs-mark price-band collar rejected it). The object form's only key today is `Stp`, whose value is `CancelNewest`, `CancelOldest` or `DecrementAndCancel`.\n\n**Treat the value as open.** Causes are added as the engine gains them, and this is not a closed enum: match the ones you handle and surface anything else verbatim rather than failing to parse. A client pinned to an older spec tag will meet causes it does not know.\n\n`GET /orders/history` reports the same causes in a **different encoding** — `OrderHistoryEntry.cancellation_reason` is always a string and renders the self-trade case as `Stp(CancelNewest)`, not as an object. Do not compare values across the two surfaces."},"timeInForce":{"type":"string"},"timestamp":{"allOf":[{"$ref":"#/components/schemas/TimestampMs"}],"description":"When the order was created, in epoch milliseconds. Named `created_at` until ENG-13314 renamed it to CCXT's `timestamp`. `datetime` beside it renders the same instant as a string."},"datetime":{"type":"string","format":"date-time","description":"`timestamp` as an ISO 8601 string in UTC, to millisecond precision. Derived from that same value rather than recorded separately, so the two cannot describe different instants. CCXT declares both; read whichever suits the client."},"lastUpdateTimestamp":{"allOf":[{"$ref":"#/components/schemas/TimestampMs"}],"description":"When the order was last modified, in epoch milliseconds. Named `updated_at` until ENG-13314 renamed it to CCXT's `lastUpdateTimestamp`."},"average":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Volume-weighted average price of this order's fills, as a decimal string. `null` when the order has not filled (`fill_totals_error` is then also `null` — nothing executed is an answer), or when the totals cannot be stated — see `fill_totals_error`."},"cost":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Total quote value executed on this order (Σ price × size), as a decimal string. Same availability rule as `average`."},"fee":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Total fee for this order as a signed decimal string: positive is a charge, negative a net maker rebate. `null` when the totals are unavailable, **and also when they are available but a contributing fill stated no fee** — a total is a claim about every fill, so one unstated leg makes the total unknown rather than smaller. Absent is never zero. `POST /orders` returns `null` here even alongside a populated `average`: that response is built from the match result, which carries no fee attribution."},"lastTradeTimestamp":{"anyOf":[{"$ref":"#/components/schemas/TimestampMs"},{"type":"null"}],"description":"Timestamp of this order's most recent fill, in epoch milliseconds. Same availability rule as `average`."},"fill_totals_error":{"type":["string","null"],"description":"Machine-readable reason `average` / `cost` / `fee` / `lastTradeTimestamp` are `null`, or `null` when they are populated **or** when the order simply has not filled. One channel for all four because they are one aggregate with one cause.\n\n`fills_retention_exceeded` — a fill of this order has aged out of the retained history, so any total would be a partial sum. A partial average is a real-looking price with nothing marking it wrong, so it is withheld.\n\n`fill_history_predates_amend` — the retained fills fall short of this order's `filled`, on a response for an order id the venue minted answering this same request. `PATCH /orders/{order_id}` is an atomic cancel-replace: it mints a new order id and carries `filled` across, while fills stay attributed to the id that executed them, so the replacement counts executions it has no history of and the totals would describe only the post-amend portion. Served only where the amend is the sole possible cause — a read route reports `fill_history_short_of_filled_qty` for the same shortfall.\n\n`fill_history_short_of_filled_qty` — the retained fills for this order id are complete as far as the retention window can tell, and still sum to less than its `filled`. Two causes produce this and they cannot be told apart here: the order descends from an amend (see `fill_history_predates_amend`), or its per-order aggregate was evicted from a bounded map and rebuilt from later fills alone, which is a retention drop the window test cannot see. The reason states the shortfall rather than naming a cause it would be guessing at, because the two have different remedies. As with every value here no partial total is served: the four fields stay `null`.\n\n`fill_history_not_retained` — the producer answering this request keeps no per-order fill history, so the totals are not derivable here even though the order has executed. Distinct from `fills_retention_exceeded`, which means a retained history dropped a fill. Reaches a client on the responses relayed from the matching engine without a fill-history join: `DELETE /orders/{order_id}`, `DELETE /orders`, and the duplicate-`client_id` replay branches of `POST /orders` and `POST /orders/batch`, which return an order the request did not create. `PATCH /orders/{order_id}` is enriched before it leaves the venue and reports `fill_history_predates_amend` instead.\n\n`filled_qty_lags_fill_history` — the retained fills for this order id sum to MORE than its `filled`, so the totals would answer for more executions than the order itself admits to. Neither a shortfall nor a retention problem: the venue folds a match's fill and the taker order's own `filled` from two separate events, so a read landing between them sees a fill history ahead of the order's projection. It closes itself within one event — retry the read. The four fields stay `null` until then, and a `filled` of `0` carrying this cause means the same thing rather than an order that has not executed: fills are known for the id, the order has simply not caught up yet."}}},"AccountSummary":{"type":"object","description":"The authenticated account's balances and open positions in one read, as served by `GET /account`. Monetary fields are lossless decimal strings. `positions` carries the full `Position` objects, including their `<field>_error` companions, so a caller does not need a second call to `/positions` — and inherits the same rule that an unavailable derived field is `null` with a reason rather than a fabricated number.","properties":{"balance":{"$ref":"#/components/schemas/Decimal"},"collateral":{"$ref":"#/components/schemas/Decimal"},"equity":{"$ref":"#/components/schemas/Decimal"},"available_margin":{"$ref":"#/components/schemas/Decimal"},"positions":{"type":"array","items":{"$ref":"#/components/schemas/Position"}}}},"CreditRequest":{"type":"object","description":"Claims synthetic USDX against the per-API-key daily allowance. Testnet only: mainnet collateral arrives through the bridge and there is no faucet or credit there. Omitting `amount` claims the whole remaining allowance for the day, so the field is a cap on the request rather than a required input.","properties":{"amount":{"$ref":"#/components/schemas/Decimal","description":"Synthetic USDX to credit (decimal string). Omit to claim the full remaining daily allowance."}}},"CreditResponse":{"type":"object","description":"What a credit claim granted, and what remains of the day's allowance. All three fields are required and all are decimal strings. `amount` is what this request credited, which can be less than what was asked for when the daily limit binds — compare `credited_today` against `daily_limit` to see the remaining headroom rather than assuming the request was granted in full.","required":["amount","credited_today","daily_limit"],"properties":{"amount":{"$ref":"#/components/schemas/Decimal","description":"USDX credited by this request (decimal string)."},"credited_today":{"$ref":"#/components/schemas/Decimal","description":"Total USDX credited to this API key so far today (decimal string)."},"daily_limit":{"$ref":"#/components/schemas/Decimal","description":"Per-API-key daily credit allowance in USDX (decimal string)."}}},"AdjustMarginResponse":{"type":"object","description":"Allocated margin and free collateral after an isolated-margin adjustment. A `200` means the engine committed both legs, and all three fields are always present.","required":["market_id","allocated_margin","collateral"],"properties":{"market_id":{"type":"string","description":"The market whose position was adjusted.","example":"BTC-USDX-PERP"},"allocated_margin":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"The position's allocated isolated margin AFTER the adjustment.","example":"350.00"},"collateral":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Free collateral AFTER the adjustment.","example":"9900.00"}}},"RateLimitBucketStatus":{"type":"object","required":["limit","remaining","reset_at_ms"],"properties":{"limit":{"type":"integer","description":"Budget of **weight** per second for this bucket — not a count of requests, and also its burst capacity, so `remaining` never exceeds it."},"remaining":{"type":"integer","description":"Tokens currently in this bucket, in **unit-cost requests**: 10 means ten weight-1 requests, or two weighing 5. Divide by an operation's weight for how many of *that* call remain. `0` is what a `429` naming this bucket reports."},"reset_at_ms":{"type":"integer","description":"Unix timestamp in milliseconds when this bucket is whole again; `0` when it is already full. It refills continuously, so tokens are available before this time."}},"description":"One rate-limit budget's state — the same three numbers, under the same names, as `RateLimitStatus`'s flat fields, so a bucket entry needs no second set of semantics. Not nullable: an entry exists only for a budget that meters the caller, so there is no unlimited case to represent — an `unlimited`-tier caller gets an empty `buckets` map instead."},"RateLimitStatus":{"type":"object","required":["tier","limit","remaining","reset_at_ms"],"properties":{"tier":{"type":"string","description":"Rate limit tier name (e.g. `pro`, `marketmaker`, `unlimited`). Tiers are multipliers on one model, not different models."},"limit":{"type":["integer","null"],"description":"Budget of request **weight** per second — not a count of requests. Most requests cost one unit, heavy aggregate reads cost more, and a batch order submit scales with its size (see `x-nexus-rate-limit-weight` and “Rate limits”). Also the burst capacity — the token bucket holds one second's worth of tokens — so `remaining` never exceeds it. Null for the unlimited tier, which is bucketed per client IP instead."},"remaining":{"type":["integer","null"],"description":"Tokens currently in the bucket, expressed as **unit-cost requests**: a value of 10 means ten weight-1 requests, or two requests weighing 5. Divide by an operation's weight to get how many of *that* call you can still make. Null for the unlimited tier."},"reset_at_ms":{"type":["integer","null"],"description":"Unix timestamp in milliseconds when the bucket refills back to `limit`; `0` when it is already full. The bucket refills continuously rather than in discrete windows, so tokens are available before this time — it is when the budget is whole again, not when the next request is permitted. Null for the unlimited tier."},"buckets":{"type":"object","description":"Every budget that meters this account, reported separately and keyed by the SAME label a `429` carries in its `bucket` field and the `X-RateLimit-Bucket` header — so `buckets[error.bucket]` resolves, and a refusal can be correlated with the state that produced it. `key` is present only when a per-key bucket actually gates the caller. `ip` never appears: it meters a connection rather than an account, and the only callers it applies to are `unlimited`-tier ones, whose whole response is null. Empty object for the `unlimited` tier, which no account-scoped bucket meters. **Additive** — the four flat fields above are unchanged and still report the binding request budget, so a client that ignores this field behaves exactly as before.","properties":{"key":{"$ref":"#/components/schemas/RateLimitBucketStatus","description":"The per-API-key request bucket. Present only when the caller presents an HMAC key with a per-key rate; the flat `remaining` above is the minimum of this and `owner`."},"owner":{"$ref":"#/components/schemas/RateLimitBucketStatus","description":"The per-account request bucket for the caller's tier — every REST operation that is not an order write."},"order":{"$ref":"#/components/schemas/RateLimitBucketStatus","description":"The trading-action bucket: `POST` and `PATCH` under `/orders`, submission and amend. Independent of the request buckets, so this is the only field that answers “how many more orders can I place” — a full `remaining` above never did."},"cancel":{"$ref":"#/components/schemas/RateLimitBucketStatus","description":"The separate cancellation bucket: `DELETE` under `/orders`. Independent of `order`, which is the guarantee that a key at its submission ceiling can still pull its quotes — read it here rather than inferring it from a submission `429`."}}}},"description":"Rate-limit state per budget. The flat `tier`, `limit`, `remaining` and `reset_at_ms` describe the **request** class only — the binding minimum of the per-key and per-owner buckets — and are unchanged. `buckets` reports each budget separately, including the trading-action and cancellation budgets order writes are charged to, so order-placement headroom is readable rather than something a client had to guess at. WebSocket ceilings are still not reported here. See “Rate limits” in the API description."},"CancelOnDisconnectStatus":{"type":"object","description":"Cancel-on-disconnect status for the authenticated account.","properties":{"enabled":{"type":"boolean","description":"The account's own COD opt-in setting."},"active":{"type":"boolean","description":"Whether COD will actually fire for this account: the account opt-in AND the exchange-side feature switch. When `enabled` is true but `active` is false, the exchange has the feature switched off and no cancel fires on disconnect."},"grace_secs":{"type":["integer","null"],"description":"Seconds the exchange waits after the last `/ws` disconnect before cancelling; a reconnect within the window disarms the cancel. Null when the feature is unavailable on this deployment."}},"required":["enabled","active"]},"SetCancelOnDisconnectRequest":{"type":"object","description":"Cancel-on-disconnect opt-in change for the authenticated account.","properties":{"enabled":{"type":"boolean","description":"True to enable COD for the account, false to disable."}},"required":["enabled"]},"Position":{"type":"object","description":"An open position with per-position risk detail. Enriched risk fields are derived strictly from indexer-mirrored state (no engine round-trip, to stay on the low-latency read path): when an input is not mirrored, the field is `null` and its companion `<field>_error` carries a machine-readable reason rather than a fabricated number. Monetary fields are lossless decimal strings; leverage fields are JSON numbers.","properties":{"market_id":{"type":"string"},"side":{"type":"string","enum":["Long","Short"]},"size":{"$ref":"#/components/schemas/Decimal"},"entry_price":{"$ref":"#/components/schemas/Decimal"},"mark_price":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The mark price this position's `unrealized_pnl`, `notional_value` and `margin_used` were computed against. This is the real blended mark that margin and liquidations use — not the trade reference that `FundingSample.mark_price` carries under the same name. Mirrored from the engine's `GET /markets/{market_id}/mark-price` by the indexer's per-cycle poll, and treated as unavailable when it is absent OR staler than the freshness bound, so a frozen last-good mark is never served as live (ENG-5909).\n\n`null` when the mark is unavailable — see `mark_price_error`. It used to be served as the string `\"0\"` in that case, a sentinel a client could not tell apart from a genuine zero; the other mark-dependent fields on this object (`notional_value`, `margin_used`, `roe`) already reported that absence as `null` + `*_error: \"mark_price_unavailable\"`, and this field now follows the same rule (ENG-13921)."},"mark_price_error":{"type":["string","null"],"description":"Machine-readable reason `mark_price` is `null` (`mark_price_unavailable`), or `null` when populated."},"unrealized_pnl":{"$ref":"#/components/schemas/Decimal"},"realized_pnl":{"$ref":"#/components/schemas/Decimal"},"liquidation_price":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Price at which the position is liquidated, as a decimal string. `null` when the venue has not computed one — see `liquidation_price_error`. A zero is a real price and never a stand-in for absence: this field used to carry the string `\"0\"` to mean \"not computed\", which made a position that liquidates only at zero — i.e. effectively cannot be liquidated — indistinguishable from one whose liquidation price is simply unknown."},"liquidation_price_error":{"type":["string","null"],"description":"Machine-readable reason `liquidation_price` is `null`. Absent, or `null`, when `liquidation_price` is populated — the indexer omits the sibling rather than nulling it, and `/account` (relayed from the engine) does not carry it at all. On the indexer-served reads it is always `margin_state_not_mirrored`: liquidation price needs margin-module state the indexer does not mirror."},"leverage":{"type":["number","null"],"description":"Position leverage (the account's leverage multiplier for this position). Currently always `null`: deriving it needs the user's leverage setting or account equity/allocated margin, which the indexer does not mirror; when `null`, `leverage_error` carries the reason. Do not infer leverage from `margin_used` — that collapses to `1/initial_margin_rate`, a per-market constant, not the real leverage."},"leverage_error":{"type":["string","null"],"description":"Machine-readable reason `leverage` is `null`, or `null` when `leverage` is populated. Currently always `margin_state_not_mirrored`."},"notional_value":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Position notional value (|size| × mark price), as a decimal string. `null` when the mark price is unavailable — see `notional_value_error`."},"notional_value_error":{"type":["string","null"],"description":"Machine-readable reason `notional_value` is `null` (e.g. `mark_price_unavailable`), or `null` when populated."},"roe":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Return on equity: `unrealized_pnl / margin_used` (return on initial margin), as a decimal string. `null` when a required input is unavailable or margin is zero — see `roe_error`."},"roe_error":{"type":["string","null"],"description":"Machine-readable reason `roe` is `null` (e.g. `mark_price_unavailable`, `margin_rate_unavailable`, `margin_used_zero`), or `null` when populated."},"margin_used":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Initial-margin requirement held against this position (`notional_value × initial_margin_rate`, under the engine's cross-margin model), as a decimal string. Isolated/custom margin allocations are not mirrored by the indexer. `null` when a required input is unavailable — see `margin_used_error`."},"margin_used_error":{"type":["string","null"],"description":"Machine-readable reason `margin_used` is `null` (e.g. `mark_price_unavailable`, `margin_rate_unavailable`), or `null` when populated."},"max_leverage":{"type":["integer","null"],"description":"Maximum leverage allowed for this market (from market risk params), as an integer matching `max_leverage` on `/markets/{market_id}/risk-params`. `null` when market params are unavailable — see `max_leverage_error`."},"max_leverage_error":{"type":["string","null"],"description":"Machine-readable reason `max_leverage` is `null` (e.g. `market_params_unavailable`), or `null` when populated."},"funding_paid":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Cumulative funding paid on this position, as a decimal string. Sign is **paid-positive**: a positive value means the position has paid funding, a negative value means it has received funding. Always present: `\"0\"` when no funding has accrued. Bounded by the funding history the indexer retains. \n\n**The sign is not uniform across the API, and this field is the exception rather than the rule.** `paid-positive` is chosen here for parity with Hyperliquid's `cumFunding`. The sibling decomposition served at `GET /positions/pnl` reports the same cash flow as `funding_pnl` with the OPPOSITE sign — received-positive, so that it sums with the other P&L components — and the service negates between the two. A client that reads both and assumes one convention will render one of them backwards, and because both are plausible numbers nothing will look wrong. Negate when moving a value between the two fields."},"contract_size":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Base-asset quantity one contract represents, mirrored from the market's parameters — the same value `GET /markets` publishes. `\"1\"` on every market listed today. `null` while market parameters have not yet loaded — see `contract_size_error`."},"contract_size_error":{"type":["string","null"],"description":"Machine-readable reason `contract_size` is `null` (`market_params_unavailable`), or `null` when populated."},"last_price":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The market's last traded price, as a decimal string. `null` before the market's first fill — see `last_price_error`.\n\n**Distinct from `mark_price`,** which is oracle-derived and is what margin and liquidation are computed against. This is the last execution. They differ, and substituting one for the other is wrong in a way that is hard to notice."},"last_price_error":{"type":["string","null"],"description":"Machine-readable reason `last_price` is `null` (`no_trades_yet`), or `null` when populated."},"opened_at":{"anyOf":[{"$ref":"#/components/schemas/TimestampMs"},{"type":"null"}],"description":"When this position was opened, in epoch milliseconds. `null` when no engine position snapshot has been folded for it yet — see `opened_at_error`.\n\n**Distinct from `updated_at`:** one is when the position began, the other when it was last written to. A position that has traded since opening has two different values here, and the open instant is the one that orders a position list by age."},"opened_at_error":{"type":["string","null"],"description":"Machine-readable reason `opened_at` is `null` (`position_snapshot_not_seen`), or `null` when populated."},"updated_at":{"$ref":"#/components/schemas/TimestampMs","description":"When this position was last written to, in epoch milliseconds. Always present — every path that creates or refreshes a position entry stamps it, so there is no absence to report and no companion `_error` field."},"margin_mode":{"type":["string","null"],"enum":["cross","isolated",null],"description":"Margin mode for this position. **Per-position, not per-account:** a market whose risk class forces isolated margin holds that mode while the same account's other positions may be cross. `null` when no engine position snapshot has been folded for it yet — see `margin_mode_error`."},"margin_mode_error":{"type":["string","null"],"description":"Machine-readable reason `margin_mode` is `null` (`position_snapshot_not_seen`), or `null` when populated."},"collateral":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Margin allocated to this position specifically, as a decimal string. Meaningful only for an isolated position. `null` — with a reason in `collateral_error` — in three cases that are deliberately kept apart: a **cross** position has no per-position allocation at all because the account backs it (`cross_margin_no_allocation`); an **isolated** position may have none posted yet (`isolated_margin_not_allocated`); and a position with no folded snapshot has one we cannot state (`position_snapshot_not_seen`). The first is an answer, not a gap."},"collateral_error":{"type":["string","null"],"description":"Machine-readable reason `collateral` is `null` (`cross_margin_no_allocation`, `isolated_margin_not_allocated`, or `position_snapshot_not_seen`), or `null` when populated."}}},"AgentRegistrationRequest":{"type":"object","description":"Delegates trading to an agent keypair without handing over the owner wallet's key. The signature is EIP-712 over `RegisterAgent{agent, expiresAt, nonce}`, produced by the **wallet** key, and the signing domain is per-network — a payload signed for one network is invalid on another and must never be replayed across them. Only `wallet`, `agent`, `nonce` and `signature` are required; note that `label` and `referral_code` sit outside the signed struct.","required":["wallet","agent","nonce","signature"],"properties":{"wallet":{"type":"string","description":"Owner wallet address (0x-prefixed, 20 bytes)"},"agent":{"type":"string","description":"Agent Ethereum address (0x-prefixed, 20 bytes) derived from the agent keypair"},"expires_at":{"type":"integer","format":"int64","description":"Expiry as Unix ms. Optional — defaults to now+30d. Must be in [now+1d, now+90d]."},"nonce":{"type":"integer","format":"int64","description":"Monotonic nonce. Use the current Unix timestamp in ms as a safe starting value."},"signature":{"type":"string","description":"EIP-712 signature over RegisterAgent{agent, expiresAt, nonce} from the wallet private key (0x-prefixed)"},"label":{"type":"string","description":"Optional human-readable label for the agent (e.g. 'my-bot')"},"referral_code":{"type":"string","description":"Referral code captured from a nexus.xyz/ref/<code> landing, if any. Not part of the signed EIP-712 payload. A missing or invalid code never fails registration; see the response's has_referrer/bound_by_this_call."}}},"AgentRegistrationResponse":{"type":"object","required":["agent_address","expires_at","scope","has_referrer","bound_by_this_call"],"properties":{"agent_address":{"type":"string","description":"The registered agent address (0x-prefixed)"},"expires_at":{"type":"integer","format":"int64","description":"Expiry as Unix ms, echoing the effective value (request value or the now+30d default)"},"scope":{"type":"string","description":"Always \"trade_only\" today — echoed so the client can show the user exactly what authority the key was granted."},"has_referrer":{"type":"boolean","description":"Whether this wallet has any referral edge at all, from this call or an earlier one. Never reveals the referrer's own identity."},"bound_by_this_call":{"type":"boolean","description":"True only the one time this exact call performed the referral bind. A retry of the same request, or a later registration on an already-bound wallet, reports has_referrer=true with bound_by_this_call=false — never a bare false that could be misread as \"binding failed\" when an edge already exists."}}},"AgentInfo":{"type":"object","description":"A registered agent key as `GET /agents` reports it.\n\n**This schema is camelCase**, unlike the snake_case native Nexus schemas around it: `expiresAt` and `registeredAt`, not `expires_at` and `registered_at`. It is not one of the CCXT-compatible schemas either, so it matches neither naming family the Schema Reference's conventions describe. Read the field names here literally rather than inferring them from the rest of the API.","properties":{"address":{"type":"string","description":"Agent address (0x-prefixed)"},"expiresAt":{"$ref":"#/components/schemas/TimestampMs","description":"Expiry Unix ms"},"registeredAt":{"$ref":"#/components/schemas/TimestampMs","description":"Registration time Unix ms"},"label":{"type":["string","null"],"description":"Optional label"}}},"Fill":{"type":"object","description":"A single trade execution **projected onto the authenticated account**, as returned by `GET /fills`. The indexer builds it per account, so `side` is THIS account's side and `taker_or_maker` is the role this account played (indexer `AccountFillRecord`).\n\nThis is not the shape an order placement returns — see `ExecutionFill` for that, and do not assume a fill from one surface parses as the other.","properties":{"id":{"type":"string","format":"uuid","description":"Fill ID"},"order_id":{"type":"string","description":"Parent order ID"},"market_id":{"type":"string","description":"Market (e.g. BTC-USDX-PERP)"},"side":{"type":"string","enum":["buy","sell"]},"price":{"$ref":"#/components/schemas/Decimal","description":"Executed price (decimal string)"},"size":{"$ref":"#/components/schemas/Decimal","description":"Executed quantity (decimal string)"},"fee":{"$ref":"#/components/schemas/Decimal","description":"Fee this fill cost the requesting account, in USDX (decimal string). Positive is a charge; negative is a maker rebate, i.e. a credit. On a self-match the account is both counterparties and settles both legs, so its single record reports the NET (taker fee minus maker rebate).\n\n**Optional — the key is OMITTED when the fill stated no fee, which is not a fee of zero.** `\"0\"` means the venue charged nothing (a liquidation fill is exempt on both legs); an absent key means it said nothing — a fill predating per-fill fee emission, or a leg the engine did not stamp. Before ENG-11832 both collapsed into `\"0\"`, and since the live `fills` WS frame already omitted the key, the two surfaces could describe the same fill differently. Treat absence as unknown, not as zero, and do not sum an absent fee into a total presented as complete.\n\nOn a self-match the NET needs both legs stated, so the key is absent if either is."},"taker_or_maker":{"type":"string","enum":["taker","maker"]},"timestamp":{"$ref":"#/components/schemas/TimestampMs","description":"Unix ms"},"is_liquidation":{"type":"boolean"}}},"Withdrawal":{"type":"object","description":"A single withdrawal record for the authenticated account","required":["id","amount","timestamp","status","tx_hash"],"properties":{"id":{"type":"string","description":"Withdrawal ID"},"amount":{"$ref":"#/components/schemas/Decimal","description":"Withdrawn amount in USDX (decimal string)"},"timestamp":{"$ref":"#/components/schemas/TimestampMs","description":"Unix ms"},"status":{"type":"string","enum":["pending","settled","failed"],"description":"Withdrawal lifecycle status"},"tx_hash":{"type":["string","null"],"description":"Settlement transaction hash (0x-prefixed hex). The key is always present and stays `null` until the withdrawal is submitted on-chain — this is the field `POST /withdrawals` tells clients to poll for."}}},"WithdrawalRequest":{"type":"object","description":"A signed `WithdrawIntent`. The server recovers the signer from `signature` and credits that wallet; nothing here selects a destination.","required":["wallet","amount","nonce","signature"],"properties":{"wallet":{"type":"string","description":"Withdrawing wallet address (0x-prefixed, 20 bytes). Must equal the address recovered from `signature`."},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Amount to withdraw, as a positive whole number of the asset's smallest unit. `WithdrawIntent` carries a `uint256`, so a fractional value cannot be represented and is refused with `400 fractional_amount`. Encode the same integer you signed."},"asset":{"type":"string","default":"USDX","description":"Asset symbol; defaults to USDX. Recorded on the withdrawal record and not forwarded to the engine. The venue is single-collateral, so the signed `asset` address is always the USDX sentinel."},"nonce":{"type":"integer","format":"int64","description":"Single-use per-wallet nonce, included in the signed typed data. The current Unix timestamp in milliseconds is a safe value. Reusing an accepted nonce returns `401 NONCE_REPLAY` and creates no second withdrawal."},"signature":{"type":"string","description":"EIP-712 signature over `WithdrawIntent{amount, asset, nonce}` from the wallet private key (0x-prefixed, 65 bytes, canonical low-S)."},"destination":{"type":"string","description":"Optional, and locked to `wallet`. The signed intent carries no destination, so any other value is refused with `400 destination_locked`. Omit it unless you are asserting the destination explicitly."}}},"WithdrawalResponse":{"type":"object","description":"Withdrawal acceptance. Carries the authoritative post-withdrawal collateral balance from the engine, plus the identity of the ledger entry the acceptance created. The withdrawal is recorded with status `pending` and is also readable through `GET /withdrawals`; this response is not proof of on-chain settlement.\n\nENG-13528 added `id`, `amount`, `status` and `timestamp`. Before that this object was `balance` alone, so a caller could not follow its own withdrawal — it had to re-read `GET /withdrawals` and guess which row was its own. `txid` is deliberately absent: no on-chain transaction exists yet at acceptance, and it appears on the `GET /withdrawals` entry once the transfer is submitted.","required":["balance","id","amount","status","timestamp"],"properties":{"balance":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Authoritative post-withdrawal collateral balance, as the engine reported it. `null` when the engine accepted the withdrawal but its body could not be read — the acceptance still stands, and the identity fields below are still served, so an unreadable balance is never reported as a fabricated number. Nullable in the schema and not only in this sentence: a generated client typed on a bare string would reject a legal response."},"id":{"type":"integer","format":"int64","description":"Identifier of the `FundsEntry` this acceptance recorded. The same `id` the entry carries on `GET /withdrawals`."},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"The accepted withdrawal amount, echoed from the signed intent."},"status":{"type":"string","enum":["pending","submitted","confirmed","failed"],"description":"Status the entry was recorded with. Always `pending` at acceptance; the other values appear only on `GET /withdrawals` after an operator transition."},"timestamp":{"allOf":[{"$ref":"#/components/schemas/TimestampMs"}],"description":"When the entry was recorded, in epoch milliseconds. The same instant the entry carries on `GET /withdrawals`."}},"additionalProperties":true},"LeverageResponse":{"type":"object","description":"The stored per-market leverage setting, echoed back from the request. Declared by ENG-13528, which found this `200` carrying an example and no `schema` — so nothing verified the body and `oasdiff` had nothing to compare it against.\n\nThis is **not** a CCXT structure. ccxt's `setLeverage` has no unified return at all (`Leverage` is what `fetchLeverage` returns, and nothing here is badged for that), so the shape gate records `setLeverage` as settled rather than as a gap, and declaring this body does not change that. The two are recorded apart on purpose.","required":["market_id","leverage"],"properties":{"market_id":{"type":"string","pattern":"^[A-Z0-9]+(-[A-Z0-9]+)*$","maxLength":64,"description":"The market the setting applies to, echoed from the request. Deliberately not renamed to CCXT's `symbol`: this object is not a CCXT structure, so the vocabulary the rest of ENG-13528 moved does not reach it."},"leverage":{"type":"integer","description":"The whole-number leverage now stored for this account and market, echoed from the request. Always within `1..=max_leverage` for the market — a value outside it is refused rather than clamped."}}},"WithdrawalError":{"type":"object","description":"Machine-readable withdrawal rejection. The classification is in `code` for every rejection the exchange raises itself; the exchange-freeze refusal carries it in `error` instead, and a forwarded engine rejection carries the same value under both. Branch on whichever is present — one always is — and treat `message` as human-readable detail only.","anyOf":[{"required":["code"]},{"required":["error"]}],"properties":{"code":{"type":"string","description":"Stable error classification. See the per-status descriptions for the documented values."},"error":{"type":"string","description":"Error classification for the freeze refusal (`withdrawals_frozen`), and a duplicate of `code` on forwarded engine rejections."},"message":{"type":"string","description":"Human-readable detail. Do not branch on it."},"claimed":{"type":"string","description":"On `SIGNER_MISMATCH`: the `wallet` the request claimed."},"recovered":{"type":"string","description":"On `SIGNER_MISMATCH`: the address actually recovered from `signature`."}}},"ExecutionFill":{"type":"object","description":"A single match as the matching engine emits it, returned inside `OrderResponse.fills` and `OrderResultOk.fills`. Names both counterparties rather than one account, because at this layer a fill is a trade between two accounts and has not been projected onto either.\n\n**It is a different shape from `Fill`, not a variant of it.** `Fill` (`GET /fills`) is the indexer's per-account projection: it carries `order_id`, `size`, `fee` and `taker_or_maker`, and its `side` is the requesting account's own side, lowercase. This schema carries `quantity` rather than `size`, no `fee`, both order ids and both account ids, and its `side` is the TAKER's side in the engine's own `Buy`/`Sell` casing. A maker's side is the opposite of the value here — the engine performs exactly that flip in `flip_fill_for_maker` before applying the fill to the maker's position, and the indexer performs it again when building the per-account record.\n\nThe two were one schema until ENG-10942, which is why a placement response documented a lowercase `side` the engine has never emitted. Every field below is always present.","required":["id","market_id","price","quantity","maker_order_id","taker_order_id","maker_account","taker_account","side","timestamp","is_liquidation"],"properties":{"id":{"type":"string","format":"uuid","description":"Trade ID for this match."},"market_id":{"type":"string","description":"Market (e.g. BTC-USDX-PERP)"},"price":{"$ref":"#/components/schemas/Decimal","description":"Executed price (decimal string)"},"quantity":{"$ref":"#/components/schemas/Decimal","description":"Executed quantity (decimal string). Named `quantity` here and `size` on `Fill`."},"maker_order_id":{"type":"string","format":"uuid","description":"The resting order's ID."},"taker_order_id":{"type":"string","format":"uuid","description":"The aggressing order's ID. This is the order you just placed when the fill arrives on a placement response."},"maker_account":{"type":"string","description":"The resting side's account, as `0x`-prefixed lowercase hex."},"taker_account":{"type":"string","description":"The aggressing side's account, as `0x`-prefixed lowercase hex."},"side":{"type":"string","enum":["Buy","Sell"],"description":"The TAKER's side, in the same casing as `Order.side`. The maker's side is the opposite; do not read this as \"my side\" without first checking which of `maker_account` / `taker_account` is yours."},"timestamp":{"$ref":"#/components/schemas/TimestampMs","description":"Unix ms"},"is_liquidation":{"type":"boolean","description":"Whether the aggressing order was a liquidation."}}},"Decimal":{"type":"string","description":"Arbitrary-precision decimal serialized as a string (lossless). Parse with a decimal type, never a float."},"TimestampMs":{"type":"integer","format":"int64","description":"Unix epoch timestamp in milliseconds."},"StatsSnapshot":{"type":"object","description":"Aggregate venue statistics. `/stats` augments the snapshot with rolling unique-trader counts.","properties":{"events_received":{"type":"integer","format":"int64"},"fills_total":{"type":"integer","format":"int64"},"liquidations_total":{"type":"integer","format":"int64"},"connected":{"type":"boolean"},"last_event_ms":{"oneOf":[{"$ref":"#/components/schemas/TimestampMs"},{"type":"null"}]},"uptime_seconds":{"type":"integer","format":"int64"},"events_per_sec":{"type":"number"},"health":{"type":"string","description":"Health classification (e.g. Healthy / Degraded / Unhealthy)."},"highest_sequence_seen":{"type":"integer","format":"int64"},"unique_traders_24h":{"type":"integer","format":"int64","description":"Rolling 24h unique traders (DAU). Present on `/stats`."},"unique_traders_7d":{"type":"integer","format":"int64","description":"Rolling 7d unique traders (WAU). Present on `/stats`."},"unique_traders_30d":{"type":"integer","format":"int64","description":"Rolling 30d unique traders (MAU). Present on `/stats`."}}},"ThroughputSample":{"type":"object","description":"One point in the venue throughput ring buffer (1s cadence, capped at 3600 points).","properties":{"timestamp":{"type":"integer","format":"int64","description":"Unix seconds."},"fills":{"type":"integer","format":"int64"}}},"ServiceHealth":{"type":"object","description":"Aggregate health for the indexer/engine/oracle/bots, consumed by status.nexus.xyz. The `services` object carries per-component detail; only the common fields are documented here.","properties":{"status":{"type":"string","enum":["ok","degraded","down","starting"],"description":"Worst-of across all components."},"updated":{"allOf":[{"$ref":"#/components/schemas/TimestampMs"}],"description":"When this reading was taken, in epoch milliseconds. Named `timestamp_ms` until ENG-13528 renamed it to CCXT's `updated`, which is what `fetchStatus` returns. The reading is stamped on every request, so it is the age of the answer rather than the age of the last status change."},"services":{"type":"object","description":"Per-component status (indexer, engine, oracle, bots). Component detail is informational and may evolve; clients should rely on the top-level `status`."}}},"EquityPoint":{"type":"object","description":"One equity sample (balance + unrealized PnL) for the account, 5s cadence.","properties":{"timestamp_ms":{"$ref":"#/components/schemas/TimestampMs"},"equity":{"type":"number","description":"Account equity at sample time."}}},"PortfolioWindow":{"type":"string","enum":["day","week","month","all"],"description":"Portfolio time-series window selector: `day`, `week`, `month`, or `all`. Shared by the `window` query parameter and the `window` echoed in the response."},"PortfolioPoint":{"type":"object","description":"One downsampled portfolio sample. Monetary fields are lossless decimal strings — parse with a decimal type, never a float.","required":["timestamp_ms","equity","pnl","volume"],"properties":{"timestamp_ms":{"$ref":"#/components/schemas/TimestampMs"},"equity":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Account equity at sample time (collateral balance + Σ unrealized PnL). Derived from the same underlying value as `EquityPoint.equity`; note `EquityPoint` serializes equity as a JSON number, whereas this is a lossless decimal string, so compare by decimal value rather than by wire representation."},"pnl":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Cumulative trading PnL up to this sample: Σ realized PnL on position close (including liquidation and ADL closes) + Σ funding (signed) + current unrealized PnL. Deposit-neutral — wallet deposits and withdrawals never move it — so the curve reflects trading performance only."},"volume":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Cumulative traded notional (Σ price × size) up to this sample, across taker and maker fills; a self-trade is counted once. Monotonically non-decreasing."}}},"PortfolioHistory":{"type":"object","description":"Portfolio time-series for the authenticated account over the requested window: equity, cumulative PnL, and cumulative volume, downsampled at a fixed per-window cadence and returned oldest first.","required":["window","cadence_ms","points"],"properties":{"window":{"allOf":[{"$ref":"#/components/schemas/PortfolioWindow"}],"description":"The window that was served — echoes the `window` query parameter, or its `day` default."},"cadence_ms":{"type":"integer","format":"int64","description":"Downsample interval between adjacent points, in milliseconds (e.g. 300000 for `day`, 86400000 for `all`)."},"points":{"type":"array","description":"Samples for the window, oldest first. Length is bounded by the window's capacity (day 288, week 168, month 120, all 366) and by the `limit` parameter.","items":{"$ref":"#/components/schemas/PortfolioPoint"}}}},"ClosedPosition":{"type":"object","description":"A closed position record.","properties":{"market_id":{"type":"string"},"side":{"type":"string","enum":["Long","Short"],"description":"The side the position was before it closed."},"size":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Absolute size at close."},"entry_price":{"$ref":"#/components/schemas/Decimal"},"exit_price":{"$ref":"#/components/schemas/Decimal"},"realized_pnl":{"$ref":"#/components/schemas/Decimal"},"closed_at_ms":{"$ref":"#/components/schemas/TimestampMs"}}},"OrderHistoryEntry":{"type":"object","description":"A terminal-status order (filled / cancelled / rejected / expired).\n\nBadged `x-ccxt-method: fetchOrders`, so it answers CCXT's unified `Order` vocabulary. It did not until ENG-14207: this is a separate indexer projection that the ENG-13314 rename never reached, so it went on serving `market_id` / `order_type` / `filled_qty` / `size` / `created_at_ms` / `completed_at_ms` while every other `Order`-returning method served the unified names. The fields CCXT has no counterpart for keep ours — `cancellation_reason` is a Nexus extension, and its encoding on this route differs from `Order`'s by design (see below).\n\nAs of ENG-14207 it serves the same unified vocabulary `Order` does, field for field, with two deliberate differences. `cancellation_reason` is a Nexus extension whose encoding on this route is a string (see below), not the object `Order` uses. And `timeInForce`, `postOnly`, `reduceOnly` are nullable here where `Order` states them unconditionally, because an entry recorded before ENG-14207 did not store them and `null` is the honest answer for one.","properties":{"id":{"type":"string","format":"uuid"},"symbol":{"type":"string","description":"The market this order was placed on. Named `market_id` until ENG-14207 renamed it to CCXT's unified name, which `Order.symbol` has carried since ENG-13314."},"side":{"type":"string","enum":["Buy","Sell"],"description":"Served as `buy` / `sell` until ENG-14207. The casing now agrees with `Order.side`: the two order surfaces used to disagree with each other, so a client reading both had to branch on which one answered. The enum is closed and the venue holds to it. The projection stores this field as text in its own lowercase vocabulary (`buy` / `sell`) and the venue translates on the way out; a stored value it cannot translate is reported as a server error rather than served, because clients compare this field literally. `type` on this same schema declares no enum for the opposite reason — an unrecognised value there crosses verbatim."},"type":{"type":"string","description":"`Limit` | `Market` | `StopLimit` | `StopMarket` | `TakeProfitLimit` | `TakeProfitMarket` | `TrailingStop` | `TrailingLimit` — the same spelling `Order.type` serves.\n\nNamed `order_type` until ENG-14207, and its VALUES moved in that same change. They were the engine enum's derived `Debug` output lowercased, so `StopLimit` reached the wire as `stoplimit`: that matched neither the `stop_*` this description used to claim nor any consumer, and the six multi-word variants resolved to nothing."},"price":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Limit price; null for market orders."},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Total quantity the order was placed for, fills included. Named `size` until ENG-14207 renamed it to CCXT's unified name, which `Order.amount` has carried since ENG-13314."},"filled":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Quantity executed, out of `amount`. Named `filled_qty` until ENG-14207 renamed it to CCXT's unified name, which `Order.filled` has carried since ENG-13314."},"remaining":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Quantity never executed — `amount - filled` — CCXT's unified `remaining`, the same field `Order.remaining` carries (ENG-13272). Served here as well because `fetchOrders` reads this projection rather than `Order`, so a CCXT client would otherwise see the field on seven methods and not on the eighth.\n\nEvery order in this projection is terminal, so this is the quantity that never executed rather than one still working: `0` on a `Filled` order, and the unexecuted remainder on a `Cancelled`, `Expired` or `Rejected` one.\n\n**Nullable here, and not on `Order`.** This projection stores `amount` and `filled` unvalidated, so either may be a value the venue cannot read back as a number; `Order` holds both as typed quantities and can always subtract them. `null` means exactly that — an operand could not be parsed, so no difference can be stated. It is deliberately not `0`: zero is a real answer meaning the order filled completely, and serving it for an unreadable record would describe that record as a completed order. Read `null` as \"unknown\", never as \"nothing left\"."},"status":{"type":"string","enum":["Filled","Cancelled","Rejected","Expired"]},"cancellation_reason":{"type":["string","null"],"description":"Why the order was cancelled or rejected; `null` for any other status. Always a **string** on this route, including for self-trade prevention, which renders as `Stp(CancelNewest)` — this surface stringifies the engine's reason type, so the encoding differs from `Order.cancellation_reason`, where that same cause is the object `{\"Stp\": \"CancelNewest\"}`. The cause set is otherwise identical, and equally open — see `Order.cancellation_reason` for the full list."},"timestamp":{"allOf":[{"$ref":"#/components/schemas/TimestampMs"}],"description":"When the order was created, in epoch milliseconds. Named `created_at_ms` until ENG-14207 renamed it to CCXT's unified name, which `Order.timestamp` has carried since ENG-13314."},"datetime":{"type":"string","format":"date-time","description":"`timestamp` as an ISO 8601 string in UTC, to millisecond precision. Derived from that same value rather than recorded separately, exactly as `Order.datetime` is. Added by ENG-14207."},"lastUpdateTimestamp":{"allOf":[{"$ref":"#/components/schemas/TimestampMs"}],"description":"When the order reached its terminal status, in epoch milliseconds. Named `completed_at_ms` until ENG-14207 renamed it to CCXT's unified name, which `Order.lastUpdateTimestamp` has carried since ENG-13314."},"clientOrderId":{"type":["string","null"],"description":"The client-supplied identifier for the order, echoed back; null when placement supplied none.\n\nAdded to this schema by ENG-14207, and `null` on an entry recorded before that change — the projection did not store the value, which is a different fact from placement not supplying one."},"timeInForce":{"type":["string","null"],"description":"`GTC` | `IOC` | `FOK` | `PostOnly` — the same spelling `Order.timeInForce` serves.\n\nAdded by ENG-14207. Nullable here where `Order.timeInForce` is not: an entry recorded before that change did not store one, and `null` says so rather than asserting the `GTC` a consumer used to assume."},"postOnly":{"type":["boolean","null"],"description":"Whether the order was placed post-only: cancelled rather than executed if it would have taken liquidity.\n\nAdded by ENG-14207. Nullable here where `Order.postOnly` is not, for the reason `timeInForce` gives: `false` on an entry that never recorded the flag would be a fabricated answer indistinguishable from a real one."},"reduceOnly":{"type":["boolean","null"],"description":"Whether the order may only reduce an existing position, echoed from placement (`OrderRequest.reduce_only`).\n\nAdded by ENG-14207. Nullable here where `Order.reduceOnly` is not — same reason as `postOnly`."},"stopPrice":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Stop price for a stop order, null otherwise.\n\nAdded to this schema by ENG-14207; `null` on an entry recorded before that change."},"triggerPrice":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The price that armed a triggered order, null for order types that are not triggered.\n\nAdded to this schema by ENG-14207; `null` on an entry recorded before that change."},"stopLossPrice":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The trigger price of a stop-loss order — CCXT's `stopLossPrice`. Set only on the stop family (`StopLimit`, `StopMarket`), where it restates the order's own `triggerPrice` under CCXT's name; null on every other order type.\n\nNexus models a bracket's stop loss as its own reduce-only child order, and records no parent→child link, so this is an order stating its OWN threshold — which is also CCXT's definition of the field. A parent order does not report its bracket's price here.\n\nTrailing types are deliberately null: their threshold is `trailing_anchor` offset by `trailing_offset_bps`, not a fixed price, so a value here would state a number the engine does not use.\n\nDOES NOT SURVIVE THE FIRE. This field is keyed on `type`, and the venue rewrites `type` when the trigger fires (`StopMarket` becomes `Market`, `StopLimit` becomes `Limit`) while retaining `triggerPrice`. The same order therefore reports a price here while it is resting and `null` once it has fired, with `triggerPrice` unchanged throughout. Reconcile a stop across its whole life on `triggerPrice`, not on this field."},"takeProfitPrice":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The trigger price of a take-profit order — CCXT's `takeProfitPrice`. Set only on the take-profit family (`TakeProfitLimit`, `TakeProfitMarket`), where it restates the order's own `triggerPrice` under CCXT's name; null on every other order type.\n\nThe mirror of `stopLossPrice`, and null under the same conditions — see that field for why a bracket parent does not report its child's price, why trailing types state nothing, and why a FIRED take profit reports null here while keeping its `triggerPrice`."},"average":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Volume-weighted average price of this order's fills, as a decimal string. `null` when the order has not filled (`fill_totals_error` is then also `null` — nothing executed is an answer), or when the totals cannot be stated — see `fill_totals_error`."},"cost":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Total quote value executed on this order (Σ price × size), as a decimal string. Same availability rule as `average`."},"fee":{"anyOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Total fee for this order as a signed decimal string: positive is a charge, negative a net maker rebate. `null` when the totals are unavailable, **and also when they are available but a contributing fill stated no fee** — a total is a claim about every fill, so one unstated leg makes the total unknown rather than smaller. Absent is never zero. `POST /orders` returns `null` here even alongside a populated `average`: that response is built from the match result, which carries no fee attribution."},"lastTradeTimestamp":{"anyOf":[{"$ref":"#/components/schemas/TimestampMs"},{"type":"null"}],"description":"Timestamp of this order's most recent fill, in epoch milliseconds. Same availability rule as `average`."},"fill_totals_error":{"type":["string","null"],"description":"Machine-readable reason `average` / `cost` / `fee` / `lastTradeTimestamp` are `null`, or `null` when they are populated **or** when the order simply has not filled. One channel for all four because they are one aggregate with one cause.\n\n`fills_retention_exceeded` — a fill of this order has aged out of the retained history, so any total would be a partial sum. A partial average is a real-looking price with nothing marking it wrong, so it is withheld.\n\n`fill_history_predates_amend` — the retained fills fall short of this order's `filled`, on a response for an order id the venue minted answering this same request. `PATCH /orders/{order_id}` is an atomic cancel-replace: it mints a new order id and carries `filled` across, while fills stay attributed to the id that executed them, so the replacement counts executions it has no history of and the totals would describe only the post-amend portion. Served only where the amend is the sole possible cause — a read route reports `fill_history_short_of_filled_qty` for the same shortfall.\n\n`fill_history_short_of_filled_qty` — the retained fills for this order id are complete as far as the retention window can tell, and still sum to less than its `filled`. Two causes produce this and they cannot be told apart here: the order descends from an amend (see `fill_history_predates_amend`), or its per-order aggregate was evicted from a bounded map and rebuilt from later fills alone, which is a retention drop the window test cannot see. The reason states the shortfall rather than naming a cause it would be guessing at, because the two have different remedies. As with every value here no partial total is served: the four fields stay `null`.\n\n`fill_history_not_retained` — the producer answering this request keeps no per-order fill history, so the totals are not derivable here even though the order has executed. Distinct from `fills_retention_exceeded`, which means a retained history dropped a fill. Reaches a client on the responses relayed from the matching engine without a fill-history join: `DELETE /orders/{order_id}`, `DELETE /orders`, and the duplicate-`client_id` replay branches of `POST /orders` and `POST /orders/batch`, which return an order the request did not create. `PATCH /orders/{order_id}` is enriched before it leaves the venue and reports `fill_history_predates_amend` instead.\n\n`filled_qty_lags_fill_history` — the retained fills for this order id sum to MORE than its `filled`, so the totals would answer for more executions than the order itself admits to. Neither a shortfall nor a retention problem: the venue folds a match's fill and the taker order's own `filled` from two separate events, so a read landing between them sees a fill history ahead of the order's projection. It closes itself within one event — retry the read. The four fields stay `null` until then, and a `filled` of `0` carrying this cause means the same thing rather than an order that has not executed: fills are known for the id, the order has simply not caught up yet.\n\n`filled_qty_unparseable` — this order's own `filled` could not be read as a decimal, so no comparison against the retained fills is possible and no total can be stated. Unique to this schema: `Order` carries `filled` as a value the venue has already parsed, while the order-history projection round-trips it through a checkpoint as text. A record reaching this cause is corrupt on the venue's side, not a retention or timing effect — retrying will not clear it. It is reported rather than folded into \"did not execute\", which is what four `null`s and a `null` error would otherwise have claimed about it."}}},"AccountFunding":{"type":"object","description":"A funding payment for the account.","properties":{"market_id":{"type":"string"},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Signed funding amount."},"direction":{"type":"string","enum":["paid","received"]},"funding_rate":{"$ref":"#/components/schemas/Decimal"},"position_size":{"$ref":"#/components/schemas/Decimal"},"timestamp":{"$ref":"#/components/schemas/TimestampMs"}}},"FundsEntry":{"type":"object","description":"A deposit or withdrawal ledger entry. ENG-13528 moved four of its properties onto CCXT's `Transaction` vocabulary: `account` → `address`, `asset` → `currency`, `tx_hash` → `txid`, `status_updated_at` → `updated`. The remaining names are Nexus extensions CCXT has no counterpart for, and keep ours.","required":["id","kind","address","amount","currency","timestamp","status","txid","updated"],"properties":{"id":{"type":"integer","format":"int64"},"kind":{"type":"string","enum":["deposit","withdrawal","faucet","credit"]},"address":{"type":"string","description":"0x-prefixed account address. Named `account` until ENG-13528 renamed it to CCXT's `address`. The rename is exact rather than approximate: this venue's account identifier IS the address the movement credits or debits."},"amount":{"$ref":"#/components/schemas/Decimal"},"currency":{"type":"string","description":"Named `asset` until ENG-13528 renamed it to CCXT's `currency`."},"timestamp":{"$ref":"#/components/schemas/TimestampMs"},"status":{"type":"string","enum":["pending","submitted","confirmed","failed"]},"txid":{"type":["string","null"],"description":"The on-chain transaction hash, null until the movement is submitted. Named `tx_hash` until ENG-13528 renamed it to CCXT's `txid`."},"updated":{"description":"Unix ms of the last admin-driven status transition. Null until the first transition. Named `status_updated_at` until ENG-13528 renamed it to CCXT's `updated`; same nullability, same meaning.","type":["integer","null"],"format":"int64"}}},"DepositRequest":{"type":"object","description":"Credits collateral to the authenticated account directly, without the bridge. Only `amount` is required; `asset` defaults to USDX. This is the allowlisted operator path, not the route real funds take — a mainnet deposit arrives through the USDX bridge and is reported by the `/bridge/deposits` read model.","required":["amount"],"properties":{"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Deposit amount (positive decimal string)."},"asset":{"type":"string","default":"USDX","description":"Asset symbol; defaults to USDX."}}},"DepositResponse":{"type":"object","description":"Engine deposit acknowledgement (forwarded). Includes the updated authoritative balance. Served by `POST /deposits` and by `POST /account/deposit`, which are the same engine handler behind two routes.","required":["balance"],"properties":{"balance":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Authoritative post-deposit balance."}},"additionalProperties":true},"DepositTarget":{"description":"Funding instructions for the authenticated account, discriminated on `mode`. `onchain` is returned only by a deployment with a real deposit-contract address configured; every other deployment returns `testnet-faucet`. The mode is a property of the deployment, so a client must branch on it rather than pin one shape.","oneOf":[{"$ref":"#/components/schemas/DepositTargetOnchain"},{"$ref":"#/components/schemas/DepositTargetTestnetFaucet"}],"discriminator":{"propertyName":"mode","mapping":{"onchain":"#/components/schemas/DepositTargetOnchain","testnet-faucet":"#/components/schemas/DepositTargetTestnetFaucet"}}},"DepositTargetOnchain":{"type":"object","description":"Funding instructions for a deployment with an on-chain deposit contract configured (mode `onchain`). Real value moves: the funds are bridged collateral, not synthetic.","required":["mode","account","asset","min_amount","onchain","confirm"],"properties":{"mode":{"type":"string","enum":["onchain"]},"account":{"type":"string","description":"The authenticated account, as `0x` + 40 hex characters. This is whose balance the instructions fund; it is derived from the credential, not from any request parameter.","example":"0x742d35cc6634c0532925a3b844bc9e7595f0beb0"},"asset":{"type":"string","description":"Collateral asset the instructions fund. USDX is the only collateral asset the venue accepts.","example":"USDX"},"min_amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Smallest funding amount the venue advises, as a decimal string. **Advisory, not enforced** — `POST /account/credit` enforces only its per-key daily cap, and nothing rejects a smaller on-chain deposit. It is the floor that makes a first trade viable, and it is deployment configuration rather than a contract constant.","example":"10"},"onchain":{"type":"object","description":"The on-chain deposit target.","required":["chain","asset","address","min_amount"],"properties":{"chain":{"type":"string","description":"Chain label the deposit contract lives on. Deployment configuration — read it, do not hardcode it.","example":"nexus-mainnet"},"asset":{"type":"string","description":"Asset the contract accepts. Only USDX is accepted; other tokens are rejected on-chain.","example":"USDX"},"address":{"type":"string","description":"Deposit-contract address, `0x` + 40 hex characters. Always well-formed: a deployment whose configured address is malformed returns `503` rather than publishing it.","example":"0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"},"min_amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Same advisory floor as the top-level `min_amount`.","example":"10"},"instructions":{"type":"string","description":"Human-readable deposit procedure. Diagnostic wording — do not match on it."}}},"confirm":{"$ref":"#/components/schemas/DepositTargetConfirm"}}},"DepositTargetTestnetFaucet":{"type":"object","description":"Funding instructions for a deployment with no on-chain deposit contract configured (mode `testnet-faucet`). The credit is **synthetic test USDX with no real-world value**, applied off-chain — no on-chain transfer occurs. Do not build a funding flow that assumes this mode exists on every network: mainnet has no faucet.","required":["mode","account","asset","min_amount","faucet","confirm"],"properties":{"mode":{"type":"string","enum":["testnet-faucet"]},"account":{"type":"string","description":"The authenticated account, as `0x` + 40 hex characters. This is whose balance the instructions fund; it is derived from the credential, not from any request parameter.","example":"0x742d35cc6634c0532925a3b844bc9e7595f0beb0"},"asset":{"type":"string","description":"Collateral asset the instructions fund. USDX is the only collateral asset the venue accepts.","example":"USDX"},"min_amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Smallest funding amount the venue advises, as a decimal string. **Advisory, not enforced** — `POST /account/credit` enforces only its per-key daily cap, and nothing rejects a smaller on-chain deposit. It is the floor that makes a first trade viable, and it is deployment configuration rather than a contract constant.","example":"10"},"faucet":{"type":"object","description":"The synthetic-credit endpoints to call. `primary` is preferred for autonomous clients — it is per-API-key and takes an amount; `alternate` is a fixed per-wallet grant.","required":["primary","alternate"],"properties":{"primary":{"type":"object","description":"Preferred: `POST /account/credit`, per-API-key and amount-taking.","required":["method","path"],"properties":{"method":{"type":"string","example":"POST"},"path":{"type":"string","example":"/account/credit"},"body":{"type":"object","description":"Request body to send, pre-filled with `min_amount`. Omit `amount` to claim the remaining daily allowance.","additionalProperties":true},"note":{"type":"string","description":"Human-readable guidance. Diagnostic wording — do not match on it."}}},"alternate":{"type":"object","description":"Alternative: `POST /faucet`, a fixed per-wallet grant claimable once per 24h.","required":["method","path"],"properties":{"method":{"type":"string","example":"POST"},"path":{"type":"string","example":"/faucet"},"note":{"type":"string","description":"Human-readable guidance. Diagnostic wording — do not match on it."}}},"disclaimer":{"type":"string","description":"States that the credit is synthetic and no on-chain transfer occurs. Surface it to a human operator rather than discarding it."}}},"confirm":{"$ref":"#/components/schemas/DepositTargetConfirm"}}},"DepositTargetConfirm":{"type":"object","description":"How to confirm the funds arrived. Identical in every mode, and the portable primitive: poll the named path until `poll_field` reflects the credit before trading.","required":["method","path","poll_field"],"properties":{"method":{"type":"string","example":"GET"},"path":{"type":"string","description":"Operation path to poll, relative to the base you are calling.","example":"/account"},"poll_field":{"type":"string","description":"Field in that response whose change confirms the funds landed.","example":"balance"},"note":{"type":"string","description":"Human-readable guidance. Diagnostic wording — do not match on it."}}},"FaucetResponse":{"type":"object","description":"Testnet faucet credit result.","properties":{"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Amount credited."},"available_at_ms":{"allOf":[{"$ref":"#/components/schemas/TimestampMs"}],"description":"Earliest time the faucet may be claimed again."}}},"PreviewResponse":{"type":"object","description":"Pre-trade preview: projects the margin/equity/fee impact of an order without submitting it.","properties":{"accepted":{"type":"boolean"},"reject_reason":{"type":["string","null"]},"required_initial_margin":{"$ref":"#/components/schemas/Decimal"},"projected_post_trade_equity":{"$ref":"#/components/schemas/Decimal"},"projected_post_trade_liquidation_price":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}]},"projected_post_trade_leverage":{"$ref":"#/components/schemas/Decimal"},"expected_fill_vwap":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}]},"projected_fees":{"$ref":"#/components/schemas/Decimal"}}},"AccountFees":{"type":"object","description":"The authenticated account's most recently mirrored fee schedule, mirroring Hyperliquid `userFees`. Reports the forward-looking schedule rate, not a realized per-fill average. There are no per-account fee tiers or discounts today, so `tier` is `base` and `discounts` is empty. Without a selector, `markets` is the per-market breakdown available from the current mirror for currently listed markets represented in the currently retained fill buffer and the top-level bps pair is only its modal summary. With `market_id`, `markets` contains only that exact target when currently listed and available, and the headline is its pair. Absence from an unfiltered `markets` list is not evidence that the account never traded a market; absence for a selected target means the exact target is unavailable, delisted, or the mirror is stale and the response fails closed as `unknown`. This response carries no source timestamp; consumers must not infer freshness from a successful read.","required":["maker_fee_bps","taker_fee_bps","tier","schedule","markets","volume_30d","volume_30d_estimated","discounts"],"properties":{"maker_fee_bps":{"type":"integer","description":"Headline maker fee in basis points. Negative means the maker is *paid* a rebate. When `schedule` is `per_market`, inspect `markets` for each available per-market rate; when it is `unknown`, zero is a sentinel rather than a known fee."},"taker_fee_bps":{"type":"integer","description":"Headline taker fee in basis points. When `schedule` is `per_market`, inspect `markets` for each available per-market rate; when it is `unknown`, zero is a sentinel rather than a known fee."},"tier":{"type":"string","minLength":1,"pattern":"\\S","description":"Fee tier for the account. Currently always `base`: there are no per-account fee tiers yet (distinct from rate-limit tiers). New values may appear when the fee model lands, so treat this as an open string."},"schedule":{"type":"string","minLength":1,"pattern":"\\S","description":"Scope of the top-level bps pair. With `market_id`, `per_market` means the exact target is currently listed and mirrored: `markets` contains its sole row and the headline is that row's pair; `unknown` means that target is unavailable, delisted, or the mirror is stale, so zero headline values are sentinels and `markets` is empty. Without a selector, `per_market` means the currently retained account-fill buffer represents at least one currently listed market with mirrored fee parameters: `markets` contains those rows and the headline is their deterministic modal pair. `reference` is unfiltered-only: the retained buffer contains no currently listed market with a mirrored schedule, so the headline is the deterministic modal pair across all available currently listed venue markets and `markets` is empty; it does not prove the account never traded a market. For both unfiltered modal calculations, the pair with the highest occurrence count wins and a tie chooses the lexicographically smallest `(maker_fee_bps, taker_fee_bps)` pair. Unfiltered `unknown` means no currently listed market fee parameters are available yet. Treat this as an open string because new scopes may appear."},"markets":{"type":"array","description":"Without `market_id`, the most recently mirrored schedule for each distinct currently listed market represented in the account's currently retained fill buffer whose fee parameters are available, sorted by `market_id`. With `market_id`, exactly one row for that target when it is currently listed and its current schedule is available. A delisted market is omitted even while its historical fills and last mirrored parameters remain retained. Returned rows are authoritative for the response's schedule scope, but the response carries no source timestamp; the unfiltered, capped in-memory fill buffer can reset or evict old markets, so neither freshness nor lifetime trading may be inferred from that mode. Empty for `reference` and `unknown`.","items":{"$ref":"#/components/schemas/AccountMarketFee"}},"volume_30d":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Rolling 30-day traded notional for the account, as a decimal string.\n\nServed from one of two sources, and `volume_30d_estimated` says what guarantee applies. Where the durable fee projection is available and caught up, this is an aggregate over a per-fill ledger sourced from the Cold Data fills tape — uncapped and durable across restarts. Otherwise it is summed from a bounded in-memory fill buffer that resets on restart, which is the only source in deployments where cold-data capture is off.\n\nParse this string; do not compare it. The scale is not part of the contract, and the two sources spell the same figure differently."},"volume_30d_estimated":{"type":"boolean","description":"`false` means `volume_30d` is not known to undercount. What that is worth depends on which source answered, and this field deliberately does not say which — a client needing a completeness guarantee should assume the weaker of the two readings below.\n\n**Durable projection:** `false` is a statement about backfill coverage, not a real-time guarantee — it means the ingest has caught up to within a few minutes of now, which implies the 30-day window is covered back to its start. It does **not** promise the most recent few minutes are included: the ingest advances in ticks, so a `false` figure can still exclude fills from just before the request. A client polling for a value to change the instant a trade lands should expect a short delay on this path.\n\n**In-memory fallback:** `true` when the source fill buffer was at capacity, so older in-window fills were evicted. `false` means no eviction was detected — it is **not** a guarantee that the full 30-day window is covered. A buffer that started empty or partial (a service instance with no history to hydrate from) undercounts too, and from the server's side that is indistinguishable from an account that simply traded little. Read the flag as a floor on the doubt, not a completeness certificate — the same semantics `total_realized_pnl_30d_estimated` carries on `GET /account/summary`."},"discounts":{"type":"array","description":"Active fee discounts applied to the account. Currently always empty — no discount program exists yet.","items":{"$ref":"#/components/schemas/FeeDiscount"}}}},"AccountMarketFee":{"type":"object","description":"The most recently mirrored maker/taker schedule for one market. In an unfiltered response the account's retained fill buffer selected the market; with `market_id` the caller selected it directly. The parent response carries no source timestamp, so consumers must not infer freshness from this row alone.","required":["symbol","maker_fee_bps","taker_fee_bps"],"properties":{"symbol":{"type":"string","pattern":"^[A-Z0-9]+(-[A-Z0-9]+)*$","maxLength":64,"description":"Market identifier, using uppercase ASCII alphanumeric segments joined by single hyphens. Named `market_id` until ENG-13528 renamed it to CCXT's `symbol`. `/account/fees` spans markets, so a row has to say which one it belongs to — the same reason `Trade.symbol` is native."},"maker_fee_bps":{"type":"integer","description":"Maker fee in basis points for this market. Negative means the maker is paid a rebate."},"taker_fee_bps":{"type":"integer","description":"Taker fee in basis points for this market."}}},"FeeDiscount":{"type":"object","description":"An active fee discount applied to the account. No discount program exists today, so `discounts` is always empty and no properties are guaranteed yet. Additional properties may be added additively if a discount program is introduced.","additionalProperties":true},"AccountPortfolioSummary":{"type":"object","description":"Portfolio summary for the authenticated account (aggregate equity, PnL, volume, open counts).","properties":{"collateral":{"$ref":"#/components/schemas/Decimal"},"total_equity":{"$ref":"#/components/schemas/Decimal"},"total_unrealized_pnl":{"$ref":"#/components/schemas/Decimal"},"total_realized_pnl_24h":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Rolling 24-hour realized PnL for the account, as a decimal string: PnL booked when a position closes, funding excluded. Best-effort — see `total_realized_pnl_24h_estimated`.","example":"380.18"},"total_realized_pnl_24h_estimated":{"type":"boolean","description":"`true` when `total_realized_pnl_24h` is known to undercount: the source closed-positions buffer was at capacity **and** its oldest retained close still falls inside the 24-hour window, so closes inside that window were evicted. Both conditions are required — a full buffer whose oldest entry is already older than the window has lost nothing inside it, and reports `false`. `false` is **not** a guarantee that the window is complete: a buffer that started empty or partial (a service instance with no history to hydrate from) undercounts too, and from the server's side that is indistinguishable from an account that simply closed few positions. Read the flag as a floor on the doubt, not a completeness certificate.","example":false},"total_realized_pnl_30d":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Rolling 30-day realized PnL for the account, as a decimal string. Counts exactly what `total_realized_pnl_24h` counts, over a longer window: PnL booked when a position closes. Funding settlements are excluded from both, so neither field matches a lifetime realized-PnL figure that folds funding in. Best-effort — see `total_realized_pnl_30d_estimated`.","example":"1420.55"},"total_realized_pnl_30d_estimated":{"type":"boolean","description":"`true` when `total_realized_pnl_30d` is known to undercount: the source closed-positions buffer was at capacity **and** its oldest retained close still falls inside the 30-day window, so closes inside that window were evicted. Same two-part test as `total_realized_pnl_24h_estimated`, against the longer cutoff — so this field can be `true` while the 24-hour one is `false` (the buffer reaches back past a day but not past a month), and whenever the 24-hour flag is `true` this one is too. `false` is **not** a guarantee that the full 30-day window is covered: a buffer that started empty or partial (a service instance with no history to hydrate from) undercounts too, and from the server's side that is indistinguishable from an account that simply closed few positions. Read the flag as a floor on the doubt, not a completeness certificate.","example":false},"total_volume_24h":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Traded notional (price × size, summed) over the last 24 hours, as a decimal string. Best-effort — see `total_volume_24h_estimated`.","example":"152340.20"},"total_volume_24h_estimated":{"type":"boolean","description":"`true` when `total_volume_24h` is known to undercount: the source fills buffer was at capacity **and** its oldest retained fill still falls inside the 24-hour window, so fills inside that window were evicted. Both conditions are required — a full buffer holding a fortnight of fills has an intact 24-hour window and reports `false`. `false` means no in-window eviction was detected, not that the window is certified complete.","example":false},"open_positions_count":{"type":"integer"},"open_orders_count":{"type":"integer"},"margin_used":{"$ref":"#/components/schemas/Decimal"},"available_margin":{"$ref":"#/components/schemas/Decimal"},"withdrawable":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Wallet-withdrawable balance: engine-authoritative free margin floored at zero (`max(0, available_margin)`). Free margin already nets each position's initial margin and pre-trade order reservations out of equity, so this is exactly what can leave the account. A negative free margin (an underwater account) is clamped to `\"0\"` and never surfaced negative. Derived from the authoritative margin view — the endpoint fails closed with `502` rather than reporting a local estimate when that view is unavailable.","example":"8500.00"},"early_access_allowed":{"type":"boolean","description":"Present only when the early-access gate is active."}}},"AccountState":{"type":"object","description":"Consolidated single-call account snapshot — the portfolio summary aggregates plus all open positions — matching Hyperliquid `clearinghouseState` ergonomics. Both parts are built from one coherent read, so `summary.open_positions_count` always equals the length of `positions`, and the embedded `summary` is identical to the standalone `/account/summary` response.","required":["summary","positions"],"properties":{"summary":{"$ref":"#/components/schemas/AccountPortfolioSummary"},"positions":{"type":"array","description":"All open positions for the account.","items":{"$ref":"#/components/schemas/Position"}}}},"BridgeAssetsResponse":{"type":"object","description":"Supported bridge chains and their deposit/withdraw assets.","required":["chains"],"properties":{"chains":{"type":"array","items":{"$ref":"#/components/schemas/BridgeChainAssets"}}}},"BridgeChainAssets":{"type":"object","description":"Bridgeable assets for one chain.","required":["chain","chain_id","deposit_assets","withdraw_assets"],"properties":{"chain":{"type":"string","description":"Chain identifier, e.g. `ethereum`."},"chain_id":{"type":["integer","null"],"format":"int64","description":"EVM chain ID of the serving network (`1` mainnet, `11155111` Sepolia/testnet); `null` on a local instance that serves no numbered chain."},"deposit_assets":{"type":"array","items":{"$ref":"#/components/schemas/BridgeDepositAsset"},"description":"Assets that can be deposited from this chain (USDC, USDX)."},"withdraw_assets":{"type":"array","items":{"$ref":"#/components/schemas/BridgeWithdrawAsset"},"description":"Assets that can be withdrawn to this chain (USDX)."}}},"BridgeDepositAsset":{"type":"object","description":"A depositable asset on a specific chain.","required":["symbol","decimals","min_amount","max_amount","confirmations"],"properties":{"symbol":{"type":"string","enum":["USDC","USDX"],"description":"Asset symbol. USDC and USDX only for this cut; USDT is out of scope."},"decimals":{"type":"integer","description":"On-chain token decimals for this asset on this chain."},"min_amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Minimum accepted for a single deposit, measured on the **delivered** amount. Provisional: still being reconciled against the provider's quoted limits (ENG-8297), so treat it as a floor that may rise."},"max_amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Maximum accepted for a single deposit. This is the vault's enforced per-transaction cap (`NexusVault.deposit` reverts above it), so a deposit over this amount fails on-chain rather than being clamped."},"confirmations":{"type":"integer","description":"Block confirmations required before a deposit is credited."}}},"BridgeWithdrawAsset":{"type":"object","description":"A withdrawable asset on a specific chain.","required":["symbol","decimals","min_amount","fee"],"properties":{"symbol":{"type":"string","enum":["USDX"],"description":"Asset symbol. USDX only for this cut."},"decimals":{"type":"integer","description":"On-chain token decimals for this asset on this chain."},"min_amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Minimum accepted for a single withdrawal. Provisional and not enforced on-chain (the release path has no floor); a UX floor only, separate from the deposit minimum and settled with the withdrawal path."},"fee":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Flat fee charged in units of the asset (may be `\"0\"`)."}}},"BridgeError":{"type":"object","description":"Error envelope returned by all non-2xx /v1/bridge responses.","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Machine-readable, stable error code (snake_case), e.g. `unsupported_chain`, `amount_below_minimum`, `deposit_not_found`."},"message":{"type":"string","description":"Human-readable description; not intended for programmatic matching."},"details":{"type":"object","additionalProperties":true,"description":"Optional structured context for the error."}}}}},"BridgeDeposit":{"type":"object","description":"A cross-chain deposit tracked by the watcher (read model). Because a deposit is created only when the watcher observes an on-chain transfer, `tx_hash`, `log_index` and `confirmations` are always present — there is no pre-arrival record.","required":["id","account_id","chain","asset","amount","credited_amount_usdx","from","to","tx_hash","log_index","status","confirmations","required_confirmations","created_at","updated_at"],"properties":{"id":{"type":"string","description":"Opaque, stable deposit id: `{tx_hash}:{log_index}`. This is the watcher's dedup key, which is what makes replays harmless."},"account_id":{"type":"string","description":"0x-prefixed Nexus account being credited."},"chain":{"type":"string","description":"Source chain, e.g. `ethereum`."},"asset":{"type":"string","enum":["USDC","USDX"],"description":"Deposited asset. USDT is out of scope in this cut."},"amount":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Deposit amount in units of `asset`."},"credited_amount_usdx":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"USDX credited to the account for this deposit. 1:1 with `amount` today."},"from":{"type":"string","description":"0x-prefixed sender address the funds left."},"to":{"type":"string","description":"0x-prefixed deposit address the funds arrived at."},"tx_hash":{"type":"string","description":"0x-prefixed source-chain transaction hash. Always present: a deposit exists only once its transfer is observed."},"log_index":{"type":"integer","description":"Log index of the transfer within `tx_hash`. Pairs with it to form `id`."},"status":{"type":"string","enum":["detected","confirming","credited","failed","reverted"],"description":"Lifecycle: `detected` → `confirming` → `credited` | `failed` | `reverted`. `detected` is 0 confirmations, `confirming` is below the required depth, `credited` is final at the required depth. The two terminal failure states are distinct: `failed` means the deposit could not be credited; `reverted` means a reorg took it back. A client distinguishes them on this field, without parsing a reason."},"confirmations":{"type":"integer","description":"Confirmations observed so far."},"required_confirmations":{"type":"integer","description":"Confirmations required before crediting."},"created_at":{"$ref":"#/components/schemas/TimestampMs"},"updated_at":{"$ref":"#/components/schemas/TimestampMs"},"credited_at":{"oneOf":[{"$ref":"#/components/schemas/TimestampMs"},{"type":"null"}],"description":"Unix ms when the deposit was credited; null until `status` is `credited`."}}},"BridgeWithdrawal":{"type":"object","description":"A Halliday withdrawal (ENG-4625). Every payout pays a Halliday one-time wallet (OTW), quoted per withdrawal; Halliday then settles to the user asynchronously in the asset and destination the user chose. There is no user-supplied payout address and no route that skips Halliday.","required":["id","account_id","amount","status","created_at","updated_at"],"properties":{"id":{"type":"string","description":"Opaque withdrawal id (`wdl_<hex>`), minted from a CSPRNG. Account-scoped: a guessed id from another account 404s."},"account_id":{"type":"string","description":"0x-prefixed Nexus account being debited."},"amount":{"type":"string","description":"Withdrawal amount in USDX base units (integer, 6 decimals), as the request supplied it."},"status":{"type":"string","enum":["pending","quoted","broadcast","confirmed","failed"],"description":"Lifecycle: pending -> quoted -> broadcast -> confirmed | failed. `quoted` once Halliday has returned an OTW, `broadcast` once the on-chain release is sent, `confirmed` once it is mined (Halliday then settles to the user). `failed` is terminal. On `failed` the Exchange debit is reversed only when the release is provably unpaid; an unknown-outcome release (broadcast, receipt unread) stays debited pending reconciliation, so `failed` does not by itself mean the balance was restored -- read `failure_reason`."},"destination":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"0x-prefixed Halliday OTW this withdrawal pays; null until `quoted`. Never a user-supplied address."},"tx_hash":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"0x-prefixed source-chain tx hash of the release; null until `confirmed`. An unknown-outcome `failed` release records its hash in `failure_reason` instead, since this field is written only on confirmation."},"confirmations":{"oneOf":[{"type":"integer"},{"type":"null"}],"description":"Confirmations observed for the release tx; null until `confirmed`."},"failure_reason":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Operator-facing reason; present only when `status` is `failed`."},"created_at":{"$ref":"#/components/schemas/TimestampMs"},"updated_at":{"$ref":"#/components/schemas/TimestampMs"}}},"BridgeWithdrawalRequest":{"type":"object","description":"Trigger a withdrawal. The only field is the amount; there is deliberately no destination_address -- the destination is the Halliday OTW quoted for this withdrawal (ENG-4625).","required":["amount"],"properties":{"amount":{"type":"string","description":"Withdrawal amount in USDX base units (a positive integer, 6 decimals). E.g. \"10000000\" is 10 USDX."}}},"SigningDomain":{"type":"object","description":"EIP-712 signing domain for a network — the domain behind `POST /agents/register` and any other typed-data signature. Network-scoped on purpose: a distinct domain per network is what makes an action signed for one network invalid on another.\n\n`chain_id` is nullable and may be absent entirely. Null or absent means the server has not published it. It does **not** mean zero, and it is not an invitation to fall back to a default or to a value cached from another network — a client that cannot obtain a `chain_id` must refuse to sign. Mainnet is the real-funds exchange running against Ethereum Mainnet rather than a Nexus L1 chain, so a Nexus L1 chain id is never correct there.","properties":{"name":{"type":"string","maxLength":128,"description":"EIP-712 domain `name`.","example":"Nexus Exchange"},"version":{"type":"string","maxLength":32,"description":"EIP-712 domain `version`.","example":"1"},"chain_id":{"type":["integer","null"],"minimum":1,"description":"EIP-712 domain `chainId` for this network. Null when the server does not publish it — refuse to sign rather than guess a value."}}},"NetworkTarget":{"type":"object","description":"Connection targets and funds semantics for one network. Field-for-field the same shape as an entry of the top-level `x-nexus-networks.networks` map — same names, same meanings — so a client can deserialize either into one type: the map is the static fallback, `/metadata` is authoritative at runtime. Two differences, both deliberate. Identity: in the static map the network identifier is the map key, while here it is also carried explicitly in `network`, so a single target stays self-describing when passed around on its own. And the static map's per-network `description` is prose for people reading this document — it is not part of the runtime payload and no edge is expected to serve it.","required":["network","rest_base"],"properties":{"network":{"type":"string","maxLength":64,"description":"Network identifier. Known values: `mainnet` (real funds), `testnet` (play funds), `local`. Deliberately an open string rather than an enum, so a network added later cannot break deserialization — but treat an identifier you do not recognize as **real funds**, and require explicit confirmation before anything that moves money.","example":"testnet"},"label":{"type":"string","maxLength":64,"description":"Human-readable name for this network, for display in UIs and CLI output. Presentation only — never parse it or key logic off it; `network` is the identifier.","example":"Testnet"},"host":{"type":"string","maxLength":255,"description":"Bare host (with port, if non-default) serving this network. Each network is its own origin, terminating its own TLS and WebSocket upgrades, so this is what belongs in CORS allowlists, certificate pinning, and egress rules. Do not assemble request URLs from it — use `rest_base`, which already carries the scheme and version segment — and never derive it by interpolating `network` into a template, because mainnet is deliberately off-pattern.","example":"api.testnet.nexus.xyz"},"rest_base":{"type":"string","format":"uri","maxLength":2048,"description":"REST base URL, version segment included. Bare operation paths from this document append to it (`/orders` → `<rest_base>/orders`). The request path is part of the HMAC canonical string, so changing base also changes what you sign.","example":"https://api.testnet.nexus.xyz/v1"},"ws_url":{"type":"string","format":"uri","maxLength":2048,"description":"WebSocket origin for this network. Market data is `<ws_url>/stream`; authenticated is `<ws_url>/ws?token=…`.","example":"wss://api.testnet.nexus.xyz"},"ws_market_data_url":{"type":"string","format":"uri","maxLength":2048,"description":"Fully-qualified public market-data WebSocket URL for this network.","example":"wss://api.testnet.nexus.xyz/stream"},"ws_authenticated_url":{"type":"string","format":"uri","maxLength":2048,"description":"Fully-qualified authenticated WebSocket URL for this network; append `?token=…` with a token minted on this same network.","example":"wss://api.testnet.nexus.xyz/ws"},"funds":{"type":"string","maxLength":32,"description":"`real` — balances are real money (mainnet: USDX bridged from Ethereum Mainnet). `play` — synthetic balances with no real-world value. Open string for the same reason as `network`: treat an unrecognized value as `real`.","example":"play"},"faucet":{"type":"boolean","description":"Whether synthetic funding is available on this network (`POST /faucet`, `POST /account/credit`). False on mainnet, where collateral arrives through the USDX bridge."},"signing_domain":{"$ref":"#/components/schemas/SigningDomain"}}},"ApiVersionMetadata":{"type":"object","description":"Body of `GET /metadata` as the edge serves it today: a single `api_version` object describing the version window this deployment accepts. Rendered once at boot from the instance's `[api_version]` configuration, so every field is always present — an unset `deprecated_below` or `sunset` is serialized as `null` rather than omitted.\n\nVersion strings are bare `MAJOR.MINOR.PATCH`. They are not the `v`-prefixed released spec tags, so compare them after stripping any prefix of your own.","required":["api_version"],"properties":{"api_version":{"type":"object","description":"The version window, and the pointers a rejected client needs in order to recover.","required":["header","min_supported","current","deprecated_below","sunset","spec_url","docs_url","policy"],"properties":{"header":{"type":"string","maxLength":64,"description":"Name of the request header the gate reads. Constant, published so a client does not have to hardcode it.","example":"x-nexus-api-version"},"min_supported":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+$","maxLength":32,"description":"Hard floor. A request whose `X-Nexus-Api-Version` parses strictly below this is rejected `426 api_version_unsupported`. `0.0.0` is the inert default and rejects nothing.","example":"0.0.0"},"current":{"type":"string","pattern":"^\\d+\\.\\d+\\.\\d+$","maxLength":32,"description":"The contract version this build targets — `info.version` of this document. Advisory: it is not part of the reject decision.","example":"0.9.57"},"deprecated_below":{"type":["string","null"],"pattern":"^\\d+\\.\\d+\\.\\d+$","maxLength":32,"description":"Upper edge of the deprecation window: a version in `[min_supported, deprecated_below)` is still served but carries `Deprecation` and `Sunset` response headers. `null` when no window is configured.","example":null},"sunset":{"type":["string","null"],"maxLength":64,"description":"The `Sunset` header value sent on deprecated-window responses, an RFC 8594 HTTP-date. `null` when unset. A configured value that is not an HTTP-date is still reported here, but the header itself is dropped rather than sent malformed.","example":null},"spec_url":{"type":"string","maxLength":2048,"description":"Where to fetch the current contract and regenerate a client. This is the self-heal target the `426` body also carries.","example":"https://github.com/nexus-xyz/nexus-exchange-api/releases"},"docs_url":{"type":"string","maxLength":2048,"description":"Human version-support policy, also sent as `Link: rel=\"deprecation\"` on deprecated-window responses.","example":"https://docs.nexus.xyz/exchange/apis-and-rates/api-versioning"},"policy":{"type":"string","maxLength":512,"description":"Prose statement of the support posture, put in the machine surface so no client can claim surprise that the floor moves pre-GA.","example":"pre-1.0: minimum-supported version may advance until GA; missing version header allowed during grace mode"}}}}},"Metadata":{"type":"object","description":"**Not served today, and referenced by no operation in this contract.** It arrived with the pre-EDR-010 re-vendor of the public spec at v0.7.2 (#5197) and describes a richer network-discovery payload no edge build has ever emitted — not one field below appears in any handler. The shape `GET /metadata` actually serves is `ApiVersionMetadata` above. Kept rather than deleted because it is the written record of the intended target-discovery surface; treat it as a proposal, not a contract, and do not generate a client from it (ENG-10661).\n\nThe original description follows.\n\nPayload of the edge's `/metadata` endpoint, served by the edge at each network's own host; this schema documents its shape so clients and agents can discover targets programmatically instead of hardcoding them.\n\nOnly `current_api_version` and `min_api_version` are required — those are what the edge has always served. Every other field is optional, so an older edge stays conformant; when one is absent, fall back to the static `x-nexus-networks` map. `signing_domain` is the one field with no safe fallback: if it is absent and you have no value for the network you are on, refuse to sign.\n\nThe response describes reachable targets only. It confers nothing: credentials are minted per network and remain invalid everywhere else, so discovering a sibling network here does not mean your keys work there.","required":["current_api_version","min_api_version"],"properties":{"current_api_version":{"type":"string","pattern":"^v\\d+\\.\\d+\\.\\d+$","maxLength":32,"description":"Latest released spec tag this edge serves.","example":"v0.7.2"},"min_api_version":{"type":"string","pattern":"^v\\d+\\.\\d+\\.\\d+$","maxLength":32,"description":"Oldest released spec tag still accepted. A client pinned below this may receive `426 Upgrade Required`; see the API version-support policy.","example":"v0.6.0"},"network":{"type":"string","maxLength":64,"description":"The network **this host** serves — the one your credentials must belong to. Known values: `mainnet`, `testnet`, `local`. An unrecognized or absent value must not be assumed to be play funds; treat it as real funds and confirm before acting.","example":"testnet"},"ws_url":{"type":"string","format":"uri","maxLength":2048,"description":"WebSocket origin for the network this host serves — the same value as `networks[network].ws_url`, inlined so a client does not have to resolve the map to connect. Market data is `<ws_url>/stream`; authenticated is `<ws_url>/ws?token=…`.","example":"wss://api.testnet.nexus.xyz"},"signing_domain":{"$ref":"#/components/schemas/SigningDomain"},"networks":{"type":"object","description":"Every network the edge knows about, keyed by network identifier, so a client can resolve another network's targets without a hardcoded host map. Keys are the same identifiers as `network`. Do not derive a host by interpolating a key into a template — mainnet is deliberately off-pattern.","additionalProperties":{"$ref":"#/components/schemas/NetworkTarget"}}}},"LiquidationEvent":{"type":"object","description":"One `payload` delivered on the per-account `liquidations` WebSocket channel. Externally tagged: exactly one property is present and its key names the engine event variant. Ignore unrecognized keys — further variants may be added.","minProperties":1,"maxProperties":1,"properties":{"LiquidationAlert":{"$ref":"#/components/schemas/LiquidationAlert"},"PortfolioLiquidation":{"$ref":"#/components/schemas/PortfolioLiquidation"}}},"LiquidationAlert":{"type":"object","description":"Pre-liquidation risk warning for one account, delivered on the `liquidations` channel. Edge-triggered: emitted once per worsening severity transition, never on recovery, and never repeated while a severity holds.","required":["account_id","market_id","severity","equity","maintenance_margin","sequence","epoch","emitted_at"],"properties":{"account_id":{"type":"string","description":"0x-prefixed address the alert is about. Always the wallet that minted the token — the channel is filtered server-side."},"market_id":{"type":["string","null"],"description":"Market the alert is scoped to, or `null` for a portfolio-level alert computed over the whole cross-margin account. Portfolio-level (`null`) is what the engine emits today, so clients must handle `null`; a non-null value scopes the alert to a single market."},"severity":{"type":"string","enum":["Warning","Critical","Imminent","Unknown"],"description":"Severity tier, classified from `equity / maintenance_margin`: `Warning` in (1.2, 1.5], `Critical` in (1.05, 1.2], `Imminent` in (1.0, 1.05]. Ordering is Warning < Critical < Imminent. `Unknown` is the forward-compatibility value for a tier this spec version does not name; treat it defensively rather than as a severity claim."},"equity":{"$ref":"#/components/schemas/Decimal","description":"Account equity at the moment of classification, as a decimal string."},"maintenance_margin":{"$ref":"#/components/schemas/Decimal","description":"Maintenance-margin requirement the equity was compared against, as a decimal string."},"sequence":{"type":"integer","format":"int64","description":"Engine event sequence number, monotonic within `epoch`."},"epoch":{"type":"integer","format":"int32","description":"Engine epoch. Increments when the engine restarts, so `sequence` is only comparable within one epoch."},"emitted_at":{"$ref":"#/components/schemas/TimestampMs","description":"When the engine emitted the event. `0` when the upstream frame carried no emit timestamp."}}},"PortfolioLiquidation":{"type":"object","description":"Terminal notification on the `liquidations` channel: the account's cross-margin positions have already been closed out. Not a warning — no action is available to the holder.","required":["account_id","closures","equity_before","equity_after","sequence","epoch","emitted_at"],"properties":{"account_id":{"type":"string","description":"0x-prefixed address that was liquidated."},"closures":{"type":"array","description":"Per-market closes that made up the liquidation. Empty only if the account held no positions.","items":{"$ref":"#/components/schemas/PortfolioLiquidationClosure"}},"equity_before":{"$ref":"#/components/schemas/Decimal","description":"Account equity before the closes, as a decimal string."},"equity_after":{"$ref":"#/components/schemas/Decimal","description":"Account equity after the closes, as a decimal string."},"sequence":{"type":"integer","format":"int64","description":"Engine event sequence number, monotonic within `epoch`."},"epoch":{"type":"integer","format":"int32","description":"Engine epoch. Increments when the engine restarts, so `sequence` is only comparable within one epoch."},"emitted_at":{"$ref":"#/components/schemas/TimestampMs","description":"When the engine emitted the event. `0` when the upstream frame carried no emit timestamp."}}},"PortfolioLiquidationClosure":{"type":"object","description":"One market's forced close within a portfolio liquidation.","required":["market_id","position_size_closed","settlement_price","settlement_amount"],"properties":{"market_id":{"type":"string"},"position_size_closed":{"$ref":"#/components/schemas/Decimal","description":"Absolute position size closed, as a decimal string."},"settlement_price":{"$ref":"#/components/schemas/Decimal","description":"Price the close settled at (the mark price used for the closure), as a decimal string."},"settlement_amount":{"$ref":"#/components/schemas/Decimal","description":"Signed collateral delta from this close after fees, as a decimal string."}}},"JurisdictionError":{"type":"object","description":"Error body returned with a `403` from a jurisdiction control. Flat `code`/`message`, matching the other top-level error bodies in this contract (`unauthorized`, `credits_frozen`) rather than the nested `BridgeError` envelope, which is scoped to `/v1/bridge`.","required":["code","message"],"properties":{"code":{"type":"string","description":"Stable machine-readable reason, identical to the `x-nexus-block-reason` response header. One of `RESTRICTED_JURISDICTION` (sanctions list — reads and writes alike), `US_RESTRICTED` (US write restriction — state-changing operations only) or `GEO_UNRESOLVED` (the request's origin could not be resolved and the write failed closed; not a statement about the caller's location). Match on this, not on `message`. All three are permanent for the caller's origin and must not be retried; treat an unrecognized code the same way."},"message":{"type":"string","description":"Human-readable explanation. Wording is not stable and is not intended for programmatic matching."}}},"PositionPnl":{"type":"object","description":"One open position's P&L, decomposed into the three components that sum to it. Served from indexer-local projections with no engine round-trip, which is why it is a separate operation rather than more fields on `Position` — `/positions` is polled hot and this is not on that path.\n\n**Signs are P&L-relative throughout, and `funding_pnl` is therefore the NEGATION of `Position.funding_paid`.** Here, positive means the account gained: a funding settlement received is positive, a fee paid is negative. `Position.funding_paid` on `/positions` is paid-positive instead, for parity with Hyperliquid's `cumFunding`. A client that reads both and assumes one convention will render one backwards, and both renderings are plausible numbers, so nothing will look wrong. Negate when moving a value between them.","properties":{"market_id":{"type":"string","description":"The market this position is in."},"side":{"type":"string","enum":["Long","Short"],"description":"Position direction."},"size":{"description":"Position size in base units.","$ref":"#/components/schemas/Decimal"},"entry_pnl":{"description":"Price P&L: `unrealized_pnl + realized_pnl`. The mark-vs-entry component plus the engine-mirrored cumulative realized figure.","$ref":"#/components/schemas/Decimal"},"unrealized_pnl":{"description":"The live mark-vs-average-entry component of `entry_pnl`.\n\n**`0` does not necessarily mean the position has no price P&L.** It is computed from `(mark, avg_entry)` and falls back to `0` whenever either is absent — the mark withheld as stale (ENG-5909), or a position carrying no `avg_entry`. Both render as a flat `0` that is indistinguishable from a position trading exactly at entry. Check `mark` and `entry` on the same payload to tell the cases apart; `total_pnl_complete` will not, because it is set unconditionally.","$ref":"#/components/schemas/Decimal"},"realized_pnl":{"description":"Engine-mirrored cumulative realized P&L for this position.","$ref":"#/components/schemas/Decimal"},"funding_pnl":{"description":"Sum of funding settlements for this `(account, market)`. **Received-positive** — the opposite sign to `Position.funding_paid`. Bounded by the funding ring buffer, so it spans retained history rather than all time.","$ref":"#/components/schemas/Decimal"},"fee_pnl":{"description":"Sum of fees for this `(account, market)`, expressed as P&L: a fee paid **reduces** P&L, so this is normally negative. Built from the engine's stamped per-fill `taker_fee` / `maker_rebate`, not re-derived from bps. Bounded by the fill ring buffer.","$ref":"#/components/schemas/Decimal"},"total_pnl":{"description":"`entry_pnl + funding_pnl + fee_pnl`.","$ref":"#/components/schemas/Decimal"},"total_pnl_complete":{"type":"boolean","description":"True when all components were derivable, so `total_pnl` is a complete sum. **Always `true` in today's responses** — but because `pnl_decomposition_json` sets it unconditionally, NOT because completeness is checked. Do not read it as a guarantee.\n\nThe gap is real and reachable: `unrealized_pnl` falls back to `0` when the mirrored mark is withheld as stale (ENG-5909 — `fresh_engine_mark_price` returns nothing past `MAX_MARK_AGE_MS`) or when the position has no `avg_entry`. In that state `total_pnl` is missing its price component while this flag still reports `true`. Prefer `mark` and `entry` on the same payload: if either is absent, treat `unrealized_pnl` and `total_pnl` as incomplete regardless of what this says.\n\nRetained as a boolean because a component could become conditionally unavailable again, and a client that already branches on it would then be correct rather than surprised. It is **not** a claim that each component spans all time: `funding_pnl` and `fee_pnl` are ring-bounded, and that retention caveat is a durable-history question rather than an availability one."}},"required":["market_id","side","size","entry_pnl","unrealized_pnl","realized_pnl","funding_pnl","fee_pnl","total_pnl","total_pnl_complete"]},"BalanceBucket":{"type":"object","description":"One balance band and the number of accounts in it.","required":["min","count"],"properties":{"min":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Inclusive lower bound of the band, in USDX collateral."},"max":{"allOf":[{"$ref":"#/components/schemas/Decimal"}],"description":"Exclusive upper bound. ABSENT on the open-ended top band, which means `>= min` — not zero."},"count":{"type":"integer","format":"int64","description":"Accounts whose current balance falls in `[min, max)`."}}},"BalanceDistribution":{"type":"object","description":"Aggregate balance distribution. Carries no per-account values by construction.","required":["buckets","account_count","network","testnet","note"],"properties":{"buckets":{"type":"array","items":{"$ref":"#/components/schemas/BalanceBucket"},"description":"Ascending bands."},"account_count":{"type":"integer","format":"int64","description":"Accounts summed across all bands."},"network":{"type":"string","maxLength":64,"description":"The chain network THIS indexer serves, taken from its required `auth.network` configuration (ENG-6443) rather than asserted by the handler. Gate on this rather than on the hostname you happened to fetch: the route lives per environment, so a URL change that nobody remembers to reflect in a downstream config would otherwise relabel the figure silently.\n\nKnown values: `mainnet` (real funds), `testnet` (play funds), `local`. Deliberately an open string rather than an enum, matching `NetworkTarget.network` and `Metadata.network`, so a network added later cannot break deserialization — but treat an identifier you do not recognize as **real funds** rather than assuming play funds.\n\n`local` and `testnet` are distinct values and both report `testnet: true` — synthetic funds either way — so this is the only field that tells a local instance apart from the public testnet.","example":"testnet"},"testnet":{"type":"boolean","description":"`true` on every network whose funds are not real — the boolean reading of `network`. Kept beside it so consumers written before ENG-12796 keep working; `network` is the more precise field and the one to prefer. It was a hardcoded `true` until ENG-12796."},"note":{"type":"string","description":"Disclaimer. Unlike the other two stats notes, this sentence is swapped outright by network rather than given a leading qualifier, because every clause of the synthetic-funds variant is false on mainnet. On a synthetic-funds network (`testnet`, `local`) it states that the balances are faucet-funded, that the projection is in-memory and resets on redeploy, and that the figures are not real funds. On `mainnet` it states the aggregation and the redeploy reset without the faucet and not-real-funds clauses."}}},"MarketCumulativeVolume":{"type":"object","description":"One market's cumulative traded notional.","required":["market_id","cumulative_volume_quote"],"properties":{"market_id":{"type":"string","description":"Market identifier, e.g. `BTC-USDX-PERP`."},"cumulative_volume_quote":{"$ref":"#/components/schemas/Decimal","description":"Cumulative traded notional in quote (USDX) for this market, as a decimal string."},"coverage_start_ms":{"oneOf":[{"$ref":"#/components/schemas/TimestampMs"},{"type":"null"}],"description":"Engine timestamp of the first fill folded into this market's total. **Omitted when unknown** — nothing has traded on this market since the indexer started. Absence means unknown; never read it as zero or as the epoch."}}},"MarketOpenInterest":{"type":"object","description":"One market's open interest. The two sides are reported separately and are never pre-summed; see the `gross_oi_two_sided_quote` field for the summed form and why it is named that way.","required":["market_id","long_oi_base","short_oi_base"],"properties":{"market_id":{"type":"string","description":"Market identifier, e.g. `BTC-USDX-PERP`. Every listed market gets an entry, carrying `\"0\"` on both sides when nothing is open, so \"no open interest\" is distinguishable from \"market unknown\". Two deliberate exceptions: a delisted market appears only while it still carries an open position, and a market whose own base sum overflowed `Decimal` gets no row at all — the only value available for it is a truncated partial sum, and the base fields are mandatory, so it is named in the venue-level `quote_error` instead."},"long_oi_base":{"$ref":"#/components/schemas/Decimal","description":"Sum of the positive position sizes in this market, in BASE units. Always present, `\"0\"` when nothing is open. Not summable across markets."},"short_oi_base":{"$ref":"#/components/schemas/Decimal","description":"Sum of the ABSOLUTE values of the negative position sizes, in BASE units — positive, like `long_oi_base`. The side is named by the field, not carried in the sign. Equals `long_oi_base` on a matched book."},"long_oi_quote":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"`long_oi_base` priced at `mark_price`, in quote (USDX). **Omitted when the market could not be priced** — see `quote_error`. Absence is never zero. One exception, and it is what stops an empty market from withholding the venue figures: when `long_oi_base` and `short_oi_base` are both `\"0\"` the notional is exactly zero at any mark, so the row carries `\"0\"` here AND a `quote_error` naming the mark problem — the number is known, the price is not."},"short_oi_quote":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"`short_oi_base` priced at `mark_price`, in quote (USDX). Present and omitted under exactly the same conditions as `long_oi_quote` — both sides price off the one mark, so they are never partially available."},"gross_oi_two_sided_quote":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"`long_oi_quote + short_oi_quote`, in quote (USDX). Named `gross` and `two_sided` so it cannot be mistaken for the one-sided figure — caption `long_oi_quote` or `short_oi_quote`, not this. **Reconciling with the BFF, which is not this field:** the BFF's per-market `open_interest` on `GET /market-stats` relays `/admin/risk-summary`, which sums raw position sizes and never multiplies by a mark, so it is in BASE units. Its analogue here is `long_oi_base + short_oi_base`. This field is that same quantity priced into quote notional, which the BFF does not publish."},"mark_price":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"The mark the quote figures were computed FROM. Two rows carry no `mark_price`: a market with nothing open whose mark was absent or stale prices to zero without consulting one, and a market whose notional overflowed did consult a mark but has no figures to attach it to — that row names the mark inside `quote_error` instead. The rule, checkable from fields that are always present: quote figures are present when `mark_price` is present, OR when `long_oi_base` and `short_oi_base` are both `\"0\"`."},"mark_price_as_of_ms":{"oneOf":[{"$ref":"#/components/schemas/TimestampMs"},{"type":"null"}],"description":"When that mark was last refreshed, on the INDEXER's clock — the same clock as `as_of_ms`, so the two are directly comparable. Present exactly when `mark_price` is."},"quote_error":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Why this market could not be priced: no mark has been mirrored yet, the mark is stale (the message carries its measured age and the limit), or the notional overflowed. Omitted when pricing succeeded. **It does not mean the quote figures are missing** — a market with nothing open carries this AND quote figures of `\"0\"`. Branch on the quote fields to decide whether a row has numbers; read this to learn what is wrong with the market's mark. One field rather than one per side, because both sides price off the same mark."}}},"CumulativeVolume":{"type":"object","description":"Cumulative traded notional in quote, venue total and per market, with the coverage start that says what it is cumulative since. Aggregate only — it carries no account id, address, or per-account volume by construction.","required":["cumulative_volume_quote","markets","network","testnet","note"],"properties":{"cumulative_volume_quote":{"$ref":"#/components/schemas/Decimal","description":"Venue-wide cumulative traded notional in quote (USDX), as a decimal string — the sum of every entry in `markets`."},"coverage_start_ms":{"oneOf":[{"$ref":"#/components/schemas/TimestampMs"},{"type":"null"}],"description":"Engine timestamp of the EARLIEST fill any market folded, so it bounds every fill the total counts. **Omitted when unknown** — nothing has traded since the indexer started, or the projection was restored from a checkpoint written before this field existed. Absence means unknown; never read it as zero or as the epoch."},"markets":{"type":"array","items":{"$ref":"#/components/schemas/MarketCumulativeVolume"},"description":"Per-market breakdown, ascending by `market_id` so two scrapes are directly comparable."},"network":{"type":"string","maxLength":64,"description":"The chain network THIS indexer serves, taken from its required `auth.network` configuration (ENG-6443) rather than asserted by the handler. Gate on this rather than on the hostname you happened to fetch: the route lives per environment, so a URL change that nobody remembers to reflect in a downstream config would otherwise relabel the figure silently.\n\nKnown values: `mainnet` (real funds), `testnet` (play funds), `local`. Deliberately an open string rather than an enum, matching `NetworkTarget.network` and `Metadata.network`, so a network added later cannot break deserialization — but treat an identifier you do not recognize as **real funds** rather than assuming play funds.\n\n`local` and `testnet` are distinct values and both report `testnet: true` — synthetic funds either way — so this is the only field that tells a local instance apart from the public testnet.","example":"testnet"},"testnet":{"type":"boolean","description":"`true` on every network whose funds are not real — the boolean reading of `network`. Kept beside it so consumers written before ENG-12796 keep working; `network` is the more precise field and the one to prefer. It was a hardcoded `true` until ENG-12796."},"note":{"type":"string","description":"Disclaimer: not a 24h window, and restarts when the service does. It opens with a network-derived qualifier rather than a fixed one — `Illustrative testnet data.` on a synthetic-funds network (`testnet`, `local`), `Mainnet data.` on `mainnet`."}}},"OpenInterest":{"type":"object","description":"Open interest, venue total and per market, with the long and short sides reported separately. Aggregate only — it carries no account id, address, or per-account position by construction. Venue figures are quote-only because base units do not add across markets.","required":["markets","mark_price_source","network","testnet","note"],"properties":{"as_of_ms":{"oneOf":[{"$ref":"#/components/schemas/TimestampMs"},{"type":"null"}],"description":"The last event this indexer ingested, on its OWN clock — the freshness of the fold as a whole, and what tells a fresh snapshot from a stalled one. **Omitted before the first event.** It is NOT the last time open interest changed: a venue where nothing traded for an hour still advances this, deliberately, because a per-position timestamp would report a quiet market as stale."},"long_oi_quote":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Venue-wide long open interest in quote (USDX) — the sum of every market's `long_oi_quote`. **This is the field to caption.** Omitted when a market carries open interest this venue could not total, and also when the venue fold itself overflows `Decimal`; `quote_error` says which. Whenever it IS present every row carries its own `long_oi_quote`, so summing the breakdown reproduces this number exactly and the total can be audited in one pass."},"short_oi_quote":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Venue-wide short open interest in quote (USDX). Equal to `long_oi_quote` by construction on a matched book; a persistent difference means the projection is desynced from the engine."},"gross_oi_two_sided_quote":{"oneOf":[{"$ref":"#/components/schemas/Decimal"},{"type":"null"}],"description":"Venue-wide `long + short` in quote (USDX) — twice the one-sided figure, named so it cannot be mistaken for the one-sided total. **There is deliberately no analogue of the BFF's `total_open_interest`:** that figure sums per-market BASE totals across markets, and base units do not add across markets, so this contract refuses to publish the quantity rather than publish a number with no unit."},"quote_error":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Why the venue totals are absent — which markets carrying open interest could not be priced, or an overflow. Omitted when the totals are present. A partial venue total is never presented as complete: one market holding open interest at an unknown price withholds all three venue figures. A market with NOTHING open never appears here even when its own mark is missing — it cannot make a total partial, and its `quote_error` stays on its own row."},"markets":{"type":"array","items":{"$ref":"#/components/schemas/MarketOpenInterest"},"description":"Per-market breakdown, ascending by `market_id` so two scrapes are directly comparable."},"mark_price_source":{"type":"string","description":"What the quote notional was priced at, in prose. Part of the contract because a notional figure without its price source is not auditable."},"network":{"type":"string","maxLength":64,"description":"The chain network THIS indexer serves, taken from its required `auth.network` configuration (ENG-6443) rather than asserted by the handler. Gate on this rather than on the hostname you happened to fetch: the route lives per environment, so a URL change that nobody remembers to reflect in a downstream config would otherwise relabel the figure silently.\n\nKnown values: `mainnet` (real funds), `testnet` (play funds), `local`. Deliberately an open string rather than an enum, matching `NetworkTarget.network` and `Metadata.network`, so a network added later cannot break deserialization — but treat an identifier you do not recognize as **real funds** rather than assuming play funds.\n\n`local` and `testnet` are distinct values and both report `testnet: true` — synthetic funds either way — so this is the only field that tells a local instance apart from the public testnet.","example":"testnet"},"testnet":{"type":"boolean","description":"`true` on every network whose funds are not real — the boolean reading of `network`. Kept beside it so consumers written before ENG-12796 keep working; `network` is the more precise field and the one to prefer. It was a hardcoded `true` until ENG-12796."},"note":{"type":"string","description":"Human-readable disclaimer, and the one-sided-versus-gross warning restated in prose."}}},"AccountFundingSnapshot":{"type":"object","required":["owner","market_id","settlement_asset","generation","cursor","observed_at_ms","accrued_through_ms","funding_window","complete","status","open_integral","closed_pending_integral","settled_cash"],"properties":{"owner":{"type":"string","pattern":"^0x[0-9a-f]{40}$","description":"Owner resolved from authentication; account selection is not accepted."},"market_id":{"type":"string"},"settlement_asset":{"type":"string","enum":["USDX"]},"generation":{"type":"string","minLength":1,"description":"Cash-counter generation. Restart, restoration or market reinitialization may replace this token; do not difference counters across generations."},"cursor":{"type":"string","pattern":"^[0-9]+$","description":"Monotonic observation sequence within a generation, encoded as a string for lossless clients. Not a WAL offset."},"observed_at_ms":{"type":"integer","format":"int64","minimum":1,"description":"UTC Unix milliseconds when the authoritative actor read the coherent state. Edge refuses observations older than 5000 ms or over 1000 ms in the future."},"accrued_through_ms":{"type":["integer","null"],"format":"int64","minimum":0,"description":"Last committed funding tick or settlement covered by the integrals, UTC Unix milliseconds. This is not the HTTP time or funding-rate sampling cadence; consumers must apply their own funding freshness budget. Null means unsampled. A complete response requires a nonzero time no later than observation and scheduled window end. After early forced settlement, the actual boundary may precede the next scheduled window start."},"funding_window":{"oneOf":[{"$ref":"#/components/schemas/AccountFundingWindow"},{"type":"null"}]},"complete":{"type":"boolean","description":"True only with complete risk state through accrued_through_ms. It does not assert that no accrual exists after that boundary."},"status":{"type":"string","enum":["complete","unsampled","unavailable"]},"open_integral":{"type":["string","null"],"pattern":"^-?[0-9]+(\\.[0-9]+)?$","description":"Signed USDX integral for the open position; positive is owed. Null is unknown."},"closed_pending_integral":{"type":["string","null"],"pattern":"^-?[0-9]+(\\.[0-9]+)?$","description":"Signed USDX integral retained after positions close; positive is owed. Flat accounts can have a nonzero value. Null is unknown."},"settled_cash":{"type":["string","null"],"pattern":"^-?[0-9]+(\\.[0-9]+)?$","description":"Cumulative actual USDX collateral cash from all committed funding settlement contributions within this generation. Positive is received, negative paid; payer caps are already reflected. Null is unknown."}},"description":"One coherent engine-owned account/market observation, including closed pending obligations. All nullable fields must be present. A complete status requires complete=true and all three monetary fields to be non-null. Unsampled or unavailable status requires complete=false and open_integral, closed_pending_integral, and settled_cash all null, including when the source would otherwise report a zero string. Contradictory source responses are rejected with FUNDING_SOURCE_INVALID (502). Monetary values are decimal strings or null; absent support is never a zero-cost response. For same-generation baselines B0/U0 and current settled cash B / sum of integrals U, interval cost is -(B-B0)+(U-U0). Actual settled cost is -(B-B0), final only once attributable pending obligations have settled."},"AccountFundingWindow":{"type":"object","required":["id","start_ms","end_ms"],"properties":{"id":{"type":"string","minLength":1},"start_ms":{"type":"integer","format":"int64","minimum":0},"end_ms":{"type":"integer","format":"int64","minimum":1}},"description":"Currently scheduled authoritative funding window. Bounds are UTC Unix milliseconds, start inclusive and end exclusive. An early forced settlement can advance this schedule ahead of the actual accrued_through_ms boundary."},"AccountFundingSourceError":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["FUNDING_SOURCE_UNAVAILABLE","FUNDING_SOURCE_INVALID","FUNDING_SOURCE_STALE"]},"message":{"type":"string"}}},"AccountFundingRequestError":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["INVALID_QUERY","MarketNotFound"]},"message":{"type":"string"}}},"AccountReferralsSummary":{"type":"object","description":"The authenticated account's own referrals summary (ENG-14757): its own referral code, plus how many accounts used it. `referred_count` is every account that redeemed the code; `active_referred_count` is the subset that has also traded at least once (a qualifying `account_activity` row) — referral-edge existence alone is never a qualifying signal for any metric beyond a raw count. **Any metric-shaped surface (a headline count, a leaderboard, a reward-adjacent badge) must render `active_referred_count`, never `referred_count`** — per PRD A-7, an unqualified referral count must never be presented as \"a real referred user.\" `referred_count` exists for a literal \"codes redeemed\" display only, if that is ever shown separately from the qualified figure.","properties":{"code":{"type":"string","nullable":true,"description":"This account's own 22-character referral code. `null` if no code has ever been minted for this account — a real, permanent state for any account that completed registration before ENG-13624 shipped `ensure_code_and_bind` on POST /agents/register, since nothing on this read path mints a code retroactively.","example":"9f2c3a1b4d5e6f708192a3"},"referred_count":{"type":"integer","format":"int32","minimum":0,"description":"Every account that redeemed this account's code, regardless of trading activity. A raw count, not a metric — see this schema's description before rendering it as one."},"active_referred_count":{"type":"integer","format":"int32","minimum":0,"description":"The subset of referred_count that has also traded at least once. This is the field any metric-shaped UI must use per PRD A-7 (a referral count read as a metric requires qualifying activity), not referred_count."}},"required":["code","referred_count","active_referred_count"]},"OrdinaryTransferRequest":{"type":"object","additionalProperties":false,"required":["source","destination","asset","amount","client_transfer_id","domain"],"properties":{"source":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"destination":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"asset":{"type":"string","enum":["USDX"]},"amount":{"type":"string","pattern":"^[0-9]+(\\.[0-9]{1,6})?$","maxLength":27,"description":"Positive USDX, at most six fractional digits and 18446744073709.551615. No rounding, signs, exponent or whitespace."},"client_transfer_id":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"Economic id scoped to the source. Never replace this id after an ambiguous response."},"domain":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$","description":"Deployment domain configured by the operator, for example prd-testnet. Signed inside the body."}}},"OrdinaryTransferEnrollment":{"type":"object","additionalProperties":false,"required":["account","domain"],"properties":{"account":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"domain":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$"}}},"OrdinaryTransferEnrollmentResponse":{"type":"object","additionalProperties":false,"required":["account","domain","enrolled"],"properties":{"account":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"domain":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$"},"enrolled":{"type":"boolean","enum":[true]}}},"OrdinaryTransferReceipt":{"type":"object","additionalProperties":false,"required":["transfer_id","status","asset","source","destination","amount","client_transfer_id","domain","committed_at_ms"],"properties":{"transfer_id":{"type":"string","pattern":"^[1-9][0-9]*$"},"status":{"type":"string","enum":["committed"]},"asset":{"type":"string","enum":["USDX"]},"source":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"destination":{"type":"string","pattern":"^(0x)?[0-9a-fA-F]{40}$"},"amount":{"type":"string","pattern":"^[0-9]+\\.[0-9]{6}$"},"client_transfer_id":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$"},"domain":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[A-Za-z0-9_-]+$"},"committed_at_ms":{"type":"integer","format":"int64","minimum":0}}},"OrdinaryTransferHistory":{"type":"object","additionalProperties":false,"required":["transfers","next_cursor","balance"],"properties":{"transfers":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/OrdinaryTransferReceipt"}},"next_cursor":{"type":["string","null"],"description":"Last returned transfer id, or null for an empty page. Pass as after; a final follow-up page may be empty."},"balance":{"type":"string","description":"Current collateral of the signed reader account, observed after the history lookup. This is an account-scoped read, not the counterparty balance or a historical receipt balance."}}},"OrdinaryTransferError":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}},"headers":{"XNextCursor":{"schema":{"type":"string"},"description":"Opaque cursor for the next page. Present only when more results exist beyond this response; absent on the last page. Pass it back via the `cursor` query parameter to fetch the next page. The response body stays a bare array — pagination state rides only in this header. Terminate a walk on this header's absence rather than on a short page: a page can be shorter than `limit` and still carry a cursor, and a page that exactly fills `limit` with nothing beyond it carries none. See “Cursor pagination” in the API description."},"XNexusBlockReason":{"schema":{"type":"string"},"description":"Which jurisdiction control refused the request: `RESTRICTED_JURISDICTION` (sanctions list; any operation, reads included), `US_RESTRICTED` (US write restriction; state-changing operations only), or `GEO_UNRESOLVED` (a state-changing operation whose origin could not be resolved, failing closed). Always equal to the body's `code` — the header exists so a browser client can branch without reading the body. Deliberately not a closed enum: treat an unrecognized value as a permanent, non-retryable refusal. See “Jurisdiction restrictions” in the API description."},"XNexusCandlesTruncated":{"schema":{"type":"string","enum":["true","false"]},"description":"Whether the `limit` hid older bars inside the requested window. `true` means the window held more bars than were returned.\n\n`false` means the limit hid nothing. It is **not** a claim that the window is fully covered: bars can also be missing because the venue has no history that far back, and `x-nexus-candles-coverage-start-ms` is what answers that question. Read the two together before concluding a quiet window was genuinely quiet.\n\nThe OHLCV body is a bare array with nowhere to put a flag, so this signal rides a header instead. `false` is emitted explicitly rather than inferred from absence, so a complete page is distinguishable from a missing signal.\n\n**Absence means unknown — never read it as `false`.** Not every `200` carries it: a request that read nothing (`limit=0`) is one case and an intermediary stripping unrecognized headers is another, and the list is not closed. None of them is evidence the page was complete."},"XNexusCandlesCoverageStartMs":{"schema":{"type":"integer","format":"int64"},"description":"Unix milliseconds UTC marking the start of historical coverage — the earliest point this venue can answer for. Present only when the server knows it.\n\n**Absence means coverage is UNKNOWN.** It does not mean zero, and it does not mean coverage is complete. This matters because an empty result is ambiguous by construction: “before we were recording” and “nothing traded in this window” return a byte-identical empty array, and only this header separates them. With it absent, an empty range must not be rendered as “no trading activity”, and a missing value must never be substituted with `0` — that would claim history back to the epoch."},"XNexusFundingTruncated":{"schema":{"type":"string","enum":["true","false"]},"description":"Whether the `limit` cut rows off this answer. `true` means the server held more funding rows for the account than were returned.\n\n`false` means the limit cut nothing. It is **not** a claim that the answer is the account's whole funding history: this endpoint reads a bounded in-memory tier, so rows older than that tier's floor are not returned no matter how large a `limit` is asked for. `x-nexus-funding-oldest-ms` is what answers that question. Read the two together before concluding an account paid no funding in a period.\n\nThe body is a bare array with nowhere to put a flag, so this signal rides a header instead. `false` is emitted explicitly rather than inferred from absence, so a complete page is distinguishable from a missing signal.\n\n**Absence means unknown — never read it as `false`.** An intermediary stripping unrecognized headers is one cause and the list is not closed. None of them is evidence the answer was complete."},"XNexusFundingOldestMs":{"schema":{"type":"integer","format":"int64"},"description":"Unix milliseconds UTC of the **oldest funding window this answer could reach** for the account — the depth of the tier that served it, not the depth of the venue's record. Present whenever that tier holds at least one row for the account.\n\nFunding rows are persisted durably, but this endpoint is served from a bounded in-memory tier that is warmed at process start and never queries the durable store per request. So rows older than this timestamp **do exist** and are **not** returned here. Treat an answer as \"history from this timestamp onward\", never as the account's complete record.\n\n**Absence means UNKNOWN.** It does not mean zero, and it does not mean the answer is complete. An empty array with this header absent must not be rendered as \"this account paid no funding\", and a missing value must never be substituted with `0` — that would claim funding history back to the epoch."}}}}