# Kronos X Member Integration Pack for Claude Code

> **Transport-specific manuals are available and are usually the better starting
> point:** `KronosX-Claude-Code-REST.md` for requests, history, backfill and the
> full RFQ/barrier endpoint contract, `KronosX-Claude-Code-WebSocket.md` for
> streaming, delivery guarantees and recovery, and `KronosX-Claude-Code-FIX.md`
> for FIX 4.4 order entry, drop copy and market data (FIX carries no RFQ
> surface). Use this combined pack when you want one file covering the native
> (REST + WebSocket) integration. The pack also carries the transport choice
> (section 1) and every FIX market-data DECISION (section 19) in full; it defers
> only the FIX tag dictionary to the FIX manual.

You are helping a broker (a venue member) build an integration against the
Kronos X MTF member API. This file is the complete, self-contained brief: drop it
into the root of your integration repository (for example as
`KronosX-Claude-Code-Integration-Pack.md`, referenced from your `CLAUDE.md`, or
paste it into your prompt) and ask Claude Code for the components you need, in
your language of choice.

Suggested first prompt after adding this file:

> Read KronosX-Claude-Code-Integration-Pack.md and generate a Kronos X client in
> <language>: signer, REST client, WebSocket client with reconnect and backfill,
> and an order state machine with a per-client position ledger seeded per the
> position seeding section. Then write a smoke test against the sandbox using my
> credentials from environment variables.

Pack revision 3.18, 21 October 2026. Aligned with member API documentation
revision 3.45 for the barrier economics of section 7 (the knock-out rebate,
the strict take profit, `premium` / `premiumBps` / `strictTakeProfit` on every
barrier view) and with revision 2.10 for everything else; the machine-readable
OpenAPI is at `/openapi.json` (`info.version`). Revision 3.18 tracks the replay
protection of member API changelog 3.85 (section 13): a signed write is accepted
once (sign every send, a retry included, afresh), `X-RECV-WINDOW` is an integer
from 1 to 60000 or refused, a timestamp may lead the venue clock by at most 5 s,
RFQ requests and quotes are idempotent on their `clOrdId` from the venue's
replay-contract line for that release, and the WebSocket
login signs the connection's challenge nonce (section 4). Revision 3.17 tracks the
cancel-all of member API changelog 3.84, in force at publication:
`DELETE /v1/orders` reads `clOrdId`, `symbol` and `side` and nothing else
(another query parameter, one given twice, another `side` or `clOrdId` with a
filter is 5003, a request body 2017), `DELETE /v1/orders/` with a trailing
slash is an empty `orderId`, 5003, never the cancel-all, and WebSocket
`order.cancelAll` reads `symbol` and `side` (`side` as on the REST route,
another `side` 5003; another member 2017, a `null` or non-string value, `args`
given twice or args that are not exactly one object 2013), each refusal cancelling nothing where
most of these requests used to cancel every open order the remaining filters
matched (the endpoint summary of section 8 and the order entry ops of section 18). Revision 3.16 tracks member API
changelog 3.83: the sandbox at `sandbox.mtf.perpetuals.net`, three nodes
behind one entry host (section 1); the leader change stated (the `5030` hint
is the published entry host, followed only when it equals yours; a node that
is not the leader closes HTTP connections but not a WebSocket; a `5030` is a
redirect and a cancel answered `5030` is resent at once; a single write's
`5001` and a `5000` during a leader change may still execute, the WebSocket
ack of an unconfirmed commit `5000` instead of `2013`; rule 9 of section 2,
sections 5, 14 and 18); `REPAIRING` without a maximum and
`GET /v1/executions` and `GET /v1/account/ledger` held below a store hole
(section 14); the cursor field ranges and the `2019` of an out-of-range
field; `acceptedTs` per surface, the single amend and cancel acks and the
cancel on disconnect as `USER_REQUESTED` (section 15); a grid change as a
re-listing and the listings table as the sandbox listing, production listing
the perpetual and the knock-out markets (section 3); a write whose commit is
not confirmed within 10 seconds ends a FIX session without a Logout (section
5). Revision 3.15 tracks the
end-of-day snapshot of member API changelog 3.81 (section 13): its CSV prints
`date` as `YYYY-MM-DD` and quotes a field per RFC 4180, `upnl` carries the
exact unrealized PnL at the snapshot's mark, the snapshot's timing is stated
as it runs (no venue-quiescent anchor, and the anchor runs again after a
restart or a takeover), and `GET /v1/executions` omits an
empty `positionEffect`. Revision 3.14 corrects two
reconciliation sentences of section 13 (member API changelog 3.80, no wire
change): the end-of-day CSV carries `positionSide`, and the venue publishes
each client sub-position's booked lifetime funding (`accFunding`), never a
per-client funding row. Revision 3.13 tracks the
budget of contained engine faults (member API changelog 3.79), no
wire schema change: ten `5005` answers of yours within 60 seconds turn every
order-budget request of yours into `1003` with `retryAfterMs` (up to 60
seconds) and the message `rate limit: repeated engine faults (5005)`,
cancellations never, and never a request that can only reduce your risk (a
reduce-only order, a batch whose every entry is reduce-only, a lot close sent
reduce-only, a barrier early close), which still consumes its order-budget
unit (the table of section 5 and the retry rule of the REST client). Revision 3.12 tracks one
listing change, no wire change: the sandbox listing of 30 September 2026
(member API changelog 3.75), eighteen rows of the table in section 3, the
perpetuals and knock-out markets of HYPE, DOGE, ZEC, NEAR, TAO, US500,
US100, XAG and USDJPY, the last a USD-settled quanto on the USD/JPY rate
(one unit pays 1 USD per 1.000 of the rate, never converted at the live
rate; member API section 2a). Revision 3.11 tracks the batch
amendment and batch cancellation routes (member API changelog 3.74):
`POST /v1/orders/batch/amend` and `POST /v1/orders/batch/cancel` with the
WebSocket ops `orders.batchAmend` and `orders.batchCancel` in the transport
matrix of section 1, the endpoint summary of section 8 and section 10, the
placement batch's per-entry `5000` and `5001` rows, and the batch rows of the
retry matrix of section 5. Revision 3.10 tracks the
bounded reads of the venue's history store (member API changelog 3.73): the new
code 5007 and the 1003 of the in-flight bound and of the node's shared budget
in the retry matrix of section 5 and the rate limits of section 12, and the
position cursor on the last page of `GET /v1/stream/events` with the
resume point and the paging-loop rule of the reconnect procedure below.
Revision 3.8 tracks one listing
change, no wire change: every `*-USD-KO` market sizes on the `qtyStep` of the
perpetual of the same underlying with `minNotional` 100 USD (member API
changelog 3.71; the thirteen barrier rows of the table in section 3 and the
close-request consequence in section 11). Revision 3.7 tracks two listing
changes, no wire change: every `*-USD-KO` market publishes `maxOrderNotional`
10000000 USD (member API changelog 3.70), the position cap on those markets
stays base-unit, and the two JPY markets are re-listed in 1,000 JPY units at
tick 0.0001 (member API section 2a). Revision 3.3 tracks a wire change: `POST /v1/margin/preview`
now prices its `orders` array as one basket of 1 to 50 entries, where
previously the figures returned described the first entry only (member API
changelog 2.10). Revision 3.2 added the transport-choice guidance with measured
latency (section 1) and the FIX market-data decision guide (section 19), with
no wire change: that venue behavior was already live at member doc revision
2.9, the pack simply did not carry it.

Everything below is normative and matches the live venue. When this file and any
other source disagree, trust the machine-readable spec at `/openapi.json` and the
interactive reference at `/docs`, then this file, in that order.

---

## 1. Environments, transports and credentials

| Environment | REST base | WebSocket base |
|---|---|---|
| Sandbox | `https://sandbox.mtf.perpetuals.net` | `wss://sandbox.mtf.perpetuals.net` |
| Production | `https://production.mtf.perpetuals.com` | `wss://production.mtf.perpetuals.com` |

- The sandbox host is `sandbox.mtf.perpetuals.net` since member API revision
  3.83: a clustered deployment of three nodes behind that one entry host,
  replacing the single-node `sandbox.mtf.perpetuals.com` from the date venue
  operations announce. Each environment publishes ONE entry host, which sends
  new connections to the node that accepts writes (the leader); node addresses
  are not published and are not member endpoints, so never pin a node. A
  leader change therefore happens on the sandbox as it can on production (rule
  9 of section 2, section 14), and venue operations stage a leader hand-over
  at an agreed time for Conformance Test Plan case I-05.
- WebSocket paths: `/ws/public` (market data, no auth) and `/ws/private`
  (account data and order entry, auth required).
- Interactive Swagger reference with in-browser signing: `<base>/docs`.
  OpenAPI 3.1: `<base>/openapi.json`.
- Credentials are an API key and secret issued by venue operations, per
  environment. NEVER hard-code them; read from environment variables (suggested:
  `KRONOSX_API_KEY`, `KRONOSX_API_SECRET`) or a secret store. Do not commit them.
- Credentials carry a **scope**. A `TRADE` key can do everything; a `READ_ONLY`
  (drop copy) key may read and subscribe but is refused on every write with code
  1005, on every transport. Use a READ_ONLY key for back-office, risk and
  reporting components so they provably cannot trade.
- **FIX 4.4** runs alongside the native pair, on `sandbox.mtf.perpetuals.net`
  port 9879 (production host and port are confirmed at onboarding), same API key
  and secret, TLS mandatory (wrap the socket before the first byte; there is no
  plaintext port). It carries order entry, drop copy AND market data
  (aggregated book plus a public trade tape, section 19). It carries no RFQ or
  barrier surface and no batch entry, amendment or cancellation, and it needs
  REST alongside it because
  recovery after a disconnect is a REST call by design (section 14). Tag-level
  contract: `KronosX-Claude-Code-FIX.md`.
- TLS only, publicly trusted chain (Let's Encrypt), no pinning needed.

**Choosing a transport.** All three transports reach the same engine through the
same validation and the same durability gate, so they accept and reject
identically. The choice is about surface and operations, never about privilege.

| | REST | WebSocket | FIX 4.4 |
|---|---|---|---|
| Order entry | yes | yes, pipelined | yes |
| Batch entry | yes | yes | no, loop singles |
| Batch amendment | yes | yes | no, loop 35=G |
| Batch cancellation | yes | yes | no, loop 35=F |
| RFQ and barriers | yes | yes | no surface |
| Market data | snapshot reads | full streaming surface, checksummed books | book and trade tape (section 19) |
| Mark, index, funding, venue events | reads | streaming | not carried |
| Recovery after a disconnect | n/a | cursor backfill | the SAME REST cursor backfill |

The native pair is the complete surface and the only one carrying RFQ, barriers,
batch entry, amendment and cancellation, mark and index prices, funding and the
typed venue-events feed.
Take FIX when an existing FIX stack is the cheaper path for your firm, not for
speed. Splitting the two is supported and common: order entry on FIX, market
data on WebSocket.

**Measured latency, stated honestly (brokers always ask).** Order-entry round
trips measured on the venue host itself, over loopback so the network is
excluded, give a venue-side p99 of 0.5 to 0.65 ms. The difference the VENUE
adds between FIX and WebSocket is about +0.16 ms at p99 on FIX. That is the
whole venue-side transport difference: fractions of a millisecond, and it will
not decide anything for you.

So a stable FIX versus WebSocket gap of tens of milliseconds (members have
reported 60 to 75 ms) does not come from the venue. It comes from the path or
from your client library. Before you open a ticket:

- **Set `TCP_NODELAY` on your FIX socket.** The venue sets it on its acceptor,
  but Nagle on YOUR side alone is enough to park a small order message until an
  unrelated round trip completes. It is coupled to real RTT, so it is invisible
  in a loopback or same-datacenter test and obvious across a WAN.
- **Compare like with like.** Pipelined WebSocket order entry against
  request-by-request FIX sends measures your client design, not the transport.
  Serializing order entry request by request and blaming venue latency is the
  classic error.
- Then look at network RTT and route, TLS session reuse, and your FIX engine's
  write path (its own send timer, batching, or a blocking send per message).

The measurement is reproducible rather than argued: the venue measures its own
side with a harness that decomposes send to engine apply, apply to durable, and
durable to acknowledgement, so venue time and path time are separated
explicitly. Ask venue operations if you want the methodology for a joint
investigation.

## 2. Non-negotiable contract rules (build these in from the start)

1. **Exact decimal strings.** Every price, quantity, rate and money amount on the
   API is an exact decimal STRING (`"65000.5"`), never a float. Parse into a
   decimal type; never use binary floating point for money.
2. **Strict order entry.** The venue REJECTS any unknown JSON field on order
   entry with code 2017 instead of ignoring it. Do not send fields this file does
   not define. Conversely, build TOLERANT response parsers: the venue adds
   response fields and event types without notice; ignore what you do not know.
3. **Grid alignment, kinds and modes.** Fetch `GET /v1/instruments` at
   startup and align every price to `tickSize` and every quantity to `qtyStep`
   before sending (reject code 2002 otherwise). Quantities are DECIMAL
   BASE-ASSET UNITS, prices are per base unit, and an order whose
   `price x qty` is below the instrument's `minNotional` rejects with 2028
   (full sizing contract in section 3). Never hard-code instrument
   parameters; the grid of a listed market never changes in place, a change
   is a re-listing (section 3). Every instrument carries `kind` (`PERP`, `SPOT`, `BARRIER` or
   `SWAP`; a SWAP market is always RFQ and its offers are taken, not quoted,
   Member API section 6h)
   and `mode` (`ORDER_BOOK` or `RFQ`): read `mode` from reference data and
   route accordingly. RFQ markets have NO order book, so order entry there
   rejects with 2022; RFQ operations on an order-book market reject with 2023
   (section 11).
4. **`clOrdId`** (your order reference): up to 36 chars, unique per member per
   24 h, echoed on every response and notification for the order's lifecycle. It
   is also an idempotency key: retrying an identical payload returns the original
   order with `"duplicate": true`; same `clOrdId` with a different payload
   rejects with 2009. The `KX-` prefix is RESERVED for venue-generated references
   and rejects with 2020. Full retry discipline in section 5. NO SURROUNDING
   WHITESPACE (Member API 3.72, in force from the date venue operations announce):
   a `clOrdId`, a `clientUid` or a Trader ID of the `reg` block that begins or ends
   with whitespace rejects with 2013 on REST, WebSocket and FIX and on the identity
   routes; trim where you build the value.
5. **`clientUid`** (your end-client reporting identifier): required on every
   order (2010 if missing), up to 64 chars, opaque, no PII in the identifier
   itself. It rides every response and notification including rejections,
   fills, cancels and venue-initiated closes, so your books reconcile per end
   client. Orders never carry client identities; where the venue reports your
   transactions under MiFIR (members that are not MiFID II investment firms),
   the RTS 22 identification behind a `clientUid` is submitted separately via
   `PUT /v1/clients/{clientUid}/identity` (member reference section 6d),
   decoupled from trading, and trading is never blocked on it.
6. **Venue-initiated activity exists.** The venue can cancel your orders and
   close your positions without a request from you (liquidation, ADL, halts).
   Executions from those closes carry `"refSource": "VENUE"` and a reference of
   the form `KX-LQ-{orderId}` or `KX-ADL-{tradeId}`. Your order/position state
   machine MUST handle fills and cancels for orders you never submitted.
7. **Leverage is your responsibility per end client.** The venue margins your
   MEMBER account on the instrument tier ladder (see `riskTiers`,
   `leverageMode` and, on FIXED-mode instruments, `maxLeverage` on
   `/v1/instruments`; `maxLeverage` is null-omitted on TIERED instruments,
   where the tier ladder governs; your account's `imMultiplier` is on
   `/v1/account`). It does not enforce per-end-client leverage; enforce your
   retail leverage limit on your side before submitting. You may send the
   optional `leverage` (integer) and `quoteCcy` (string) fields as ASSERTIONS:
   the venue validates them against your effective margin and rejects a false
   assertion with 2018; they never change the margin applied.
8. **HTTP status vs `code`.** Every response carries the envelope
   `{ code, msg, data, asOfSeq, ts }`. `code: 0` means accepted by the gateway,
   but an order can still be REJECTED by the engine inside a 200: check
   `data.status == "REJECTED"` and `data.rejectCode`. Treat both paths.
9. **Cluster leader redirects (5030).** On clustered deployments, among them
   the clustered sandbox of member API revision 3.83 from the date venue
   operations announce, a request that is not a `GET`
   under `/v1` (a read-only `POST` such as `POST /v1/margin/preview` included)
   sent to a node that is not the leader answers HTTP 503 with `code: 5030`,
   `msg: "NOT_LEADER"` before the route runs, and is never forwarded, so it did
   not take effect and cannot reach the book later; the body carries
   `leaderId` and `leader`, an `https` base URL that on every venue deployment
   is the environment's published entry host (`leaderId` 0 and `leader` null
   while no leader is known). Such a node closes every HTTP connection after
   its answer (`Connection: close`, GOAWAY on HTTP/2), so your next request
   reaches the leader; it does NOT close a WebSocket opened on it, where every
   write op acks 5030 (with `leaderId` and `leader` under `data`): close that
   socket yourself and reconnect. This is a redirect, never a fault and never a
   rejection: accept `leader` only when it equals the entry host you
   configured and resubmit there with the SAME `clOrdId` (idempotency protects
   the retry), do not back off, never count it as a reject; a cancel,
   cancel-all, amendment or cancellation batch answered 5030 did not take
   effect, so send it again at once. If `leader` names any other host, send no
   credentials there, use your configured entry host and tell venue
   operations. Member API section 10.7.

## 3. Contract sizing and quantities (decimal base units)

Every quantity on this API, order `qty`, RFQ `qty`, position `netQty`, book
depth, executions, FIX `OrderQty(38)`, is a **decimal number of base-asset
units**. On `BTC-USD-PERP`, `"qty": "0.0014"` means 0.0014 BTC. Integer
quantity strings remain valid everywhere (`"1"` is 1 base unit). Prices are
quoted **per base unit** (per BTC, per ETH), so the notional of an order is
simply `price x qty` in the settlement currency.

Three per-market listing properties on `GET /v1/instruments` govern granularity
and minimum size. Drive ALL rounding and validation from the live values; never
hard-code them:

- `qtyStep`, the quantity grid: every `qty` must be an EXACT multiple of it.
  A `qty` that is not, at any decimal depth, rejects with 2002; the venue never
  rounds a quantity for you.
- `minQty`, the smallest accepted quantity. On every current listing `minQty`
  equals `qtyStep`.
- `minNotional`, the smallest accepted order value: an order or RFQ whose
  `price x qty` falls below it rejects with **2028** (5 USD on every current
  listing, 100 USD on the barrier markets since member API revision 3.71).
  Valuation rules:
  - Priced orders are valued at their limit price.
  - Market orders, and the market legs of conditionals, are valued at the
    current MARK price.
  - Amends re-check the FULL amended quantity at the effective price
    (market-type conditionals at the current mark, untriggered limit children
    at their child limit price, resting limits at the amended price).
  - RFQs are checked twice: at create, valued at the current mark, and again
    on every quote, valued at the quoted entry price. An accept needs no
    further check because the accepted price cannot change.
  - **Two exemptions, both closing-only:** venue-initiated closes
    (liquidation, ADL) bypass the floor, and your own `reduceOnly` orders and
    amends within your reducible position bypass it too. A position whose
    whole notional has fallen below the floor is therefore ALWAYS closable:
    submit a `reduceOnly` order of exactly the position size. Never oversize
    the close: a `reduceOnly` order larger than your reducible position
    rejects with 2008. Orders that could OPEN or grow exposure always face
    the floor.

**Current listings** (informative; ALWAYS read the live values from
`GET /v1/instruments`, per environment):

| Market | Price tick | `qtyStep` = `minQty` | `minNotional` | `maxPositionNotional` | `maxOrderNotional` |
|---|---|---|---|---|---|
| BTC-USD-PERP | 0.5 | 0.0001 | 5 | 2000000000 | 10000000 |
| ETH-USD-PERP | 0.05 | 0.001 | 5 | 2000000000 | 10000000 |
| SOL-USD-PERP | 0.01 | 0.01 | 5 | 2000000000 | 10000000 |
| BNB-USD-PERP | 0.05 | 0.001 | 5 | 2000000000 | 10000000 |
| XRP-USD-PERP | 0.0001 | 1 | 5 | 2000000000 | 10000000 |
| HYPE-USD-PERP | 0.01 | 0.01 | 5 | 2000000000 | 10000000 |
| DOGE-USD-PERP | 0.00001 | 1 | 5 | 2000000000 | 10000000 |
| ZEC-USD-PERP | 0.05 | 0.001 | 5 | 2000000000 | 10000000 |
| NEAR-USD-PERP | 0.0001 | 1 | 5 | 2000000000 | 10000000 |
| TAO-USD-PERP | 0.01 | 0.01 | 5 | 2000000000 | 10000000 |
| SP500-USD-PERP | 0.1 | 0.001 | 5 | 2000000000 | 10000000 |
| NDX100-USD-PERP | 0.1 | 0.001 | 5 | 2000000000 | 10000000 |
| XAU-USD-PERP | 0.1 | 0.001 | 5 | 2000000000 | 10000000 |
| WTI-USD-PERP | 0.001 | 0.01 | 5 | 2000000000 | 10000000 |
| BRENT-USD-PERP | 0.001 | 0.01 | 5 | 2000000000 | 10000000 |
| EUR-USD-PERP | 0.0001 | 1 | 5 | 2000000000 | 10000000 |
| GBP-USD-PERP | 0.0001 | 1 | 5 | 2000000000 | 10000000 |
| JPY-USD-PERP | 0.0001 | 1 (= 1,000 JPY) | 5 | 2000000000 | 10000000 |
| US500-USD-PERP | 0.1 | 0.001 | 5 | 2000000000 | 10000000 |
| US100-USD-PERP | 0.1 | 0.001 | 5 | 2000000000 | 10000000 |
| XAG-USD-PERP | 0.001 | 0.01 | 5 | 2000000000 | 10000000 |
| USDJPY-USD-PERP | 0.001 | 0.01 (quanto, 1 USD per 1.000) | 5 | 2000000000 | 10000000 |
| BTC-USD-KO (barrier, RFQ) | 0.5 | 0.0001 | 100 | not set (unit caps apply) | 10000000 |
| ETH-USD-KO (barrier, RFQ) | 0.05 | 0.001 | 100 | not set (unit caps apply) | 10000000 |
| SOL-USD-KO (barrier, RFQ) | 0.01 | 0.01 | 100 | not set (unit caps apply) | 10000000 |
| BNB-USD-KO (barrier, RFQ) | 0.05 | 0.001 | 100 | not set (unit caps apply) | 10000000 |
| XRP-USD-KO (barrier, RFQ) | 0.0001 | 1 | 100 | not set (unit caps apply) | 10000000 |
| HYPE-USD-KO (barrier, RFQ) | 0.01 | 0.01 | 100 | not set (unit caps apply) | 10000000 |
| DOGE-USD-KO (barrier, RFQ) | 0.00001 | 1 | 100 | not set (unit caps apply) | 10000000 |
| ZEC-USD-KO (barrier, RFQ) | 0.05 | 0.001 | 100 | not set (unit caps apply) | 10000000 |
| NEAR-USD-KO (barrier, RFQ) | 0.0001 | 1 | 100 | not set (unit caps apply) | 10000000 |
| TAO-USD-KO (barrier, RFQ) | 0.01 | 0.01 | 100 | not set (unit caps apply) | 10000000 |
| SP500-USD-KO (barrier, RFQ) | 0.1 | 0.001 | 100 | not set (unit caps apply) | 10000000 |
| NDX100-USD-KO (barrier, RFQ) | 0.1 | 0.001 | 100 | not set (unit caps apply) | 10000000 |
| XAU-USD-KO (barrier, RFQ) | 0.1 | 0.001 | 100 | not set (unit caps apply) | 10000000 |
| WTI-USD-KO (barrier, RFQ) | 0.001 | 0.01 | 100 | not set (unit caps apply) | 10000000 |
| BRENT-USD-KO (barrier, RFQ) | 0.001 | 0.01 | 100 | not set (unit caps apply) | 10000000 |
| EUR-USD-KO (barrier, RFQ) | 0.0001 | 1 | 100 | not set (unit caps apply) | 10000000 |
| GBP-USD-KO (barrier, RFQ) | 0.0001 | 1 | 100 | not set (unit caps apply) | 10000000 |
| JPY-USD-KO (barrier, RFQ) | 0.0001 | 1 (= 1,000 JPY) | 100 | not set (unit caps apply) | 10000000 |
| US500-USD-KO (barrier, RFQ) | 0.1 | 0.001 | 100 | not set (unit caps apply) | 10000000 |
| US100-USD-KO (barrier, RFQ) | 0.1 | 0.001 | 100 | not set (unit caps apply) | 10000000 |
| XAG-USD-KO (barrier, RFQ) | 0.001 | 0.01 | 100 | not set (unit caps apply) | 10000000 |
| USDJPY-USD-KO (barrier, RFQ) | 0.001 | 0.01 (quanto, 1 USD per 1.000) | 100 | not set (unit caps apply) | 10000000 |
The two cap columns are decimal settlement-currency strings (USD on every
current listing). A market that does not publish a cap serves the field absent
or null; the legacy base-unit caps then apply (see below).

The table is the SANDBOX listing (member API section 2a). Production lists the
perpetual and the knock-out markets, but not every row of this table: as
planned today, the thirteen perpetuals listed before the 30 September 2026
listing, the knock-out markets of BTC, ETH, SOL, BNB, XRP and JPY and the six
swap markets; none of the eighteen rows of the 30 September 2026 listing, none
of the other seven knock-out rows and none of the spot markets. The venue
confirms the production instrument list with the go-live listing, and on every
market both environments list, the production configuration as planned today
carries the values of this table. The values production publishes on
`GET /v1/instruments` from its first day are the binding ones.

**A grid change is a re-listing** (member API section 2a, revision 3.83). The
venue never changes the `tickSize` or the `qtyStep` of a listed market in
place: every resting order and open position is held on the market's grid, so
the venue's listing controls refuse such a change on every market kind. A
re-listing is announced in writing in advance, naming the market, the new
values and the date. On the date the market is delisted (`venueEvents`
`STOP_TRADING` with `tradingState` `DELISTED`, reason `DELISTING` and
`cancelsResting` true; every working and pending-trigger order cancelled with
`cancelReason` `INSTRUMENT_HALT`, on FIX `150=4` with `Text(58)`
`INSTRUMENT_HALT`; new orders and amends `3006`, reduce-only orders included,
so close what you do not want final-settled before the date), open positions
are final-settled at one close-out price (open barrier contracts within their
terms, `terminationReason` `FINAL_SETTLEMENT`; open RFQs stay but can no longer
be quoted or accepted), and the market is listed again with the new values and
an empty book under the symbol and `isin` the announcement names (`2001` for a
kept symbol between the removal and the new listing; anything off the new grid
`2002`, never rounded). No field or frame announces a re-listing before the
date and no member surface serves an instrument id: key on the symbol, re-read
`GET /v1/instruments` after the announced date and on any unexpected `2001`,
`2002`, `3006` or `DELISTED` status, and carry no cached grid or book state
across the date. `minNotional` is not part of the grid: it may change in place,
resting orders stay, and the new floor applies to new orders, amends and RFQs.

**Worked example.** `BTC-USD-PERP`, tick `0.5`, `qtyStep` `0.0001`,
`minNotional` `5`: buy `0.0014` BTC at `74000` is a notional of
`74000 x 0.0014 = 103.60` USD, well above the 5 USD floor. A `qty` of
`"0.00145"` on the same market rejects with 2002 (14.5 steps is not an exact
multiple of `0.0001`), and so does `"0.00005"` (below the step); a valid step
that prices out below 5 USD, for example `"0.0001"` at a 40000 mark, rejects
with 2028 instead.

**Note on XRP-USD-PERP.** With `qtyStep` = `minQty` = 1 XRP, small whole-XRP
quantities price out below the 5 USD floor at typical marks (at a 0.6 mark the
smallest order that clears the floor is 9 XRP). The floor gates orders that
open or grow exposure; partial fills can still leave a position smaller than
that, and such a residual stays closable with an exact-size `reduceOnly` order
as described above.

**Order and position limits.** Both are notional first with base-unit
fallbacks. Code your risk gate to these rules exactly:

- **Valuation.** Value priced orders (including the limit leg of a
  conditional) at their LIMIT price; value market orders, market-type
  conditionals and RFQ creation at the CURRENT mark; value RFQ quotes at the
  quoted entry price; re-check amends on the FULL amended quantity at the
  effective price. The position cap's worst case (your net plus same-direction
  working orders plus the incoming order) is always valued at the CURRENT
  mark. Size against the live mark, never a cached one.
- **Position cap (2011).** Where the market publishes `maxPositionNotional`,
  a worst-case value beyond it rejects **2011**, which gives every market the
  same dollar capacity so smaller markets no longer pin first in a correlated
  move. The cap gates INCREASES only: risk-reducing flow (`reduceOnly`,
  venue-initiated closes) is never blocked, and a position pushed above the
  cap by mark drift alone is never forced to reduce; further risk-increasing
  orders just reject 2011 until the valuation is back under the cap.
- **Order cap (2013).** Where the market publishes `maxOrderNotional`, one
  order's `price x qty` at its effective price beyond it rejects **2013**.
  One deliberate asymmetry: `reduceOnly` orders are NOT exempt from the order
  cap, so close a big position in SEVERAL messages, each under the cap;
  venue-initiated closes are exempt. On top of the published cap there is an
  absolute size ceiling on every market: `price x qty` must stay within
  `1000000000000000000` settlement-currency units (the venue's exact-money
  range). It is orders of magnitude above every published `maxOrderNotional`,
  so you will never meet it; it is stated so the rule has no gap.
- **Legacy fallback.** On a market without a notional cap the legacy
  base-unit checks (`maxPositionQty`, `maxOrderQty`) apply unchanged. Where a
  notional cap IS set it alone decides and the unit field is informational
  legacy; in particular, a per-member `maxOrderQty` override configured by
  venue operations is NOT consulted on a notional-capped market, so never
  derive a limit from it there.

Spot markets (BTC-USD, ETH-USD, SOL-USD, BNB-USD, XRP-USD on the sandbox,
Member API revision 3.50) trade on the same fractional `qtyStep` as the
perpetual of the same underlying (0.0001 BTC, 0.001 ETH and BNB, 0.01 SOL, 1
XRP), so `price x qty` is the exact cash leg and `qty` the exact asset leg;
holdings appear in `assets` on `GET /v1/account`, never in positions.

## 4. Authentication (HMAC signing recipe)

Private REST requests carry three headers:

```
X-API-KEY:    <your api key>
X-TIMESTAMP:  <unix milliseconds as string>
X-SIGNATURE:  hex_lowercase(HMAC_SHA256(secret, timestamp + METHOD + pathWithQuery + body))
```

- `METHOD` uppercase (`GET`, `POST`, `PUT`, `DELETE`).
- `pathWithQuery` is the path plus query string exactly as sent, no host
  (example: `/v1/orders?symbol=BTC-USD-PERP`).
- `body` is the exact raw JSON string you transmit, empty string when no body.
  **Sign the SAME bytes you send.** Serialize once, sign that string, transmit
  that string; never re-serialize between signing and sending, and never let
  your HTTP stack rewrite the URL after you signed it.
- Optional `X-RECV-WINDOW` (ms, default 5000, member API revision 3.85): an
  integer from 1 to 60000, any other value is refused with 1002 (it was reduced to
  60000). The timestamp may lag the venue clock by up to the window and lead it by
  at most 5000 ms (or the window when smaller); outside that the request rejects
  with 1002.
- **A signed write is accepted ONCE** (member API revision 3.85): the same
  `X-API-KEY` and `X-SIGNATURE` on any method but GET answers 1002 `request
  already received` and nothing runs, and a venue node refuses 1002 a write stamped
  before it started or became the leader. **Sign every send, a retry included,
  with a fresh timestamp** and keep its `clOrdId`; never let an HTTP library
  resend a signed request on its own; two intentionally identical writes need
  different timestamps.

Worked example (verify your signer against it):

```
secret     = "test-secret"
timestamp  = "1700000000000"
method     = "GET"
path       = "/v1/account"
body       = ""
payload    = "1700000000000GET/v1/account"
signature  = HMAC_SHA256("test-secret", payload) hex lowercase
           = 741b61b6d3740ede7864551612095d2fb73b75a4f70534f45e9e207bdd1f321c
```

Second vector, a GET with a comma-bearing query string transmitted RAW (not
`%2C`), no body. The venue verifies against the query exactly as it arrives on
the wire. Some libraries, notably Python `aiohttp`/`yarl`, re-normalize
percent-encoding AFTER you build the URL, which flips commas between `,` and
`%2C` and turns every signed query-string GET into a 1001. With aiohttp use
`yarl.URL(url, encoded=True)` or sign the exact final string on the wire. If
your stack sends `%2C`, sign `%2C`; if it sends a raw comma, sign the raw
comma. Reproducing the hex proves your signer, not your transport: after it
matches, verify in your library's request tracing that the transmitted query
is byte-identical to what you signed. A 1001 on signed GETs with a query
string, while bodied POSTs work, is the classic signature of an HTTP layer
re-encoding after signing. This bites first on the gap-frame backfill query
(section 14), whose ready-made `channels` list contains raw commas.

```
secret     = "test-secret"
timestamp  = "1700000000000"
method     = "GET"
path       = "/v1/stream/events?fromSeq=904118&channels=execution,margin,order,wallet&limit=500"
body       = ""
signature  = 821b87a9ee51c26503856f18c0faa712b093aef4ec387e0ef1ecb1ea550651ac
```

WebSocket private auth after connect (member API revision 3.85: a server
challenge, mandatory). The first frame of every `/ws/private` connection is its
challenge; read it, keep its nonce, then send `auth` with the nonce as the fourth
arg:

```
<- {"channel":"challenge","type":"challenge","nonce":"<32 lowercase hex>","ts":<venue ms>}
-> {"op":"auth","reqId":"1","args":[apiKey, expiresMs, signature, nonce]}
signature = hex_lowercase(HMAC_SHA256(secret, "GET/ws/private" + expiresMs + nonce))
```

where `expiresMs` is unix milliseconds a few seconds in the future, at most 60 s
ahead of the venue clock (number or numeric string; it is concatenated as its
decimal string form), and `nonce` is the challenge's 32 characters exactly as
received. Worked example: `secret=my-api-secret`, `expiresMs=1700000060000`,
`nonce=5f2b8c0e9a4d4e7c8b1f3a6d2c9e0b7a` gives
`d9e79457cc986b400928993ae3e89cd4e3a0f5fdeeb035d4a3768430fcbf0980`. The frame
verifies only on the connection whose challenge it signs: the old three-arg frame
answers 1001 `auth failed: sign the nonce of this connection's challenge frame
and send it as the fourth arg (Member API section 10.1)`, another connection's
nonce 1001 `auth failed: the nonce is not this connection's challenge`. A second
login on the same connection signs the same nonce; after a reconnect sign the new
connection's challenge, never an old nonce. Never log the auth frame.

## 5. Idempotency, duplicates and safe retries

**`clOrdId` is the idempotency key for order entry** (REST `POST /v1/orders`,
every batch entry, WS `order.place`, FIX `ClOrdID(11)`): up to 36 chars, unique
per member per 24 h. An omitted or empty `clOrdId` is accepted but has NO
duplicate protection, so a blind retry can double the order: always send one.
On FIX the same holds, and one consequence is worth building for: an order sent
without `ClOrdID(11)` is accepted, and every ExecutionReport for it OMITS tag 11
entirely rather than carrying an empty one. Do not require tag 11 on inbound
reports.

**How long the venue holds a reference.** Uniqueness for 24 h is your obligation.
What the venue retains is the most recent 4,000,000 references venue-wide, or
24 h, whichever is reached first, so on a day of exceptional venue-wide volume the
effective duplicate-detection horizon is shorter than 24 h. A released reference is
simply unknown: resending it places a NEW order instead of returning the original.
A reference whose order is still working or pending trigger is never released, so
cancel and replace by `clOrdId` work for the whole life of the order. Retries are a
seconds-scale operation and are unaffected; reconcile older state by `orderId` or
`GET /v1/orders`, never by resending.

- Resending the IDENTICAL payload (same field values) returns the original
  order with `"duplicate": true` and `code` 0. This is the safe retry.
- The same `clOrdId` with a DIFFERENT payload rejects at the envelope level:
  HTTP 400, `code` 2009, the reference echoed in `data.clOrdId`. The WS
  `order.place` ack mirrors it (`success: false`, code 2009). If you see 2009
  on what you believed was an identical retry, your serialization is not
  stable (field values changed between attempts); the FIRST order is live, so
  read it back before doing anything else.
- The `KX-` prefix (any casing) is reserved for venue references (2020).
- **On FIX both branches are ExecutionReports on the session that sent the
  order,** never silence: the different-payload rejection is `150=8` with
  `9010=2009`, and the identical-payload retry restates the ORIGINAL order as
  `150=I` (order status) with venue tag `9015=Y`, which is the FIX spelling of
  `"duplicate": true`. Neither is a lifecycle event, so neither appears in the
  event stream or a backfill; correlate both by `ClOrdID(11)`. See
  `KronosX-Claude-Code-FIX.md` section 5.
- **`POST /v1/rfq` is idempotent on its `clOrdId` since member API revision
  3.85.** A request or a quote whose clOrdId you already used answers 2009 and
  opens or replaces nothing (from the venue's replay-contract line for that
  release), with no restatement of the original: after a timeout resend with the
  SAME clOrdId, signed afresh, and on 2009 read `GET /v1/rfq/mine` (requests) or
  `GET /v1/rfq/quotes/mine` (quotes). Without a clOrdId, or before that line, a
  blind resend opens a second, identical RFQ: reconcile with `GET /v1/rfq/mine`
  and cancel the duplicate (cancels are free). A re-quote that replaces your live
  quote carries a NEW clOrdId. An RFQ accept that times out is resolved by
  READING (`/v1/rfq/mine`, `/v1/executions`), never by blind resends;
  re-accepting a filled RFQ answers 2024 and changes nothing.

**Retry matrix** (codes in section 17):

| Outcome | Action |
|---|---|
| 1002 clock skew | Sync your clock (NTP), retry with a fresh timestamp, same `clOrdId`. Since member API revision 3.85 also `request already received` (the same signed bytes sent again: sign the retry afresh, never resend the bytes), `request signed before this node began to accept writes` (sign it again), and an `X-RECV-WINDOW` outside 1 to 60000 or a timestamp more than 5 s ahead (fix the header or the clock first). |
| 1003 rate limit | Wait EXACTLY the returned `retryAfterMs` (section 12) (for a history read the wait is a suggestion, and a repeated 1003 means your reads overlap: serialise them; one while no other read of yours of the group is in flight is the node's shared budget: back off), then resend the same message, same `clOrdId`. A 1003 with the message `rate limit: repeated engine faults (5005)` follows ten `5005` answers within 60 seconds: the wait is up to 60 seconds, and the commands that answered `5005` are never resent unchanged. Cancellations and the requests that can only reduce your risk (a reduce-only order, a batch whose every entry is reduce-only, a lot close sent reduce-only, a barrier early close) are not refused by that budget. |
| 5001 engine busy | The command was NOT accepted. Retry with backoff and jitter, same `clOrdId`. One exception on a clustered deployment: a SINGLE write (REST or WebSocket) answered 5001 while its node lost the leadership may already be in the venue's log and may still execute on the new leader at its original terms (member API section 10.7, revision 3.83 states it), so during a leader change it is INDETERMINATE, see below. A batch row's 5001 never is such a write. |
| 5007 history store busy | Reads: retry after data.retryAfterMs, with backoff; after repeats on the same read, narrow it. |
| 5030 NOT_LEADER | A redirect, never a fault or a rejection: when the returned `leader` equals the entry host you configured, resubmit there with the SAME `clOrdId` immediately, no backoff (a cancel or an amendment at once: it did not take effect); otherwise send no credentials to it, use your configured entry host and tell venue operations. Never count it as a reject (rule 9 of section 2). |
| 5000 internal error | Reads: retry. Order entry: INDETERMINATE, see below. On a clustered deployment a write whose commit the venue did not confirm within 10 seconds answers 5000 on REST (HTTP 500) and, since member API revision 3.83, on the WebSocket, where it acked 2013 "malformed request" before. |
| 5005 command failed | Never resubmit unchanged: a deterministic venue fails an identical request identically. The response reports what the command DID achieve before it stopped, so reconcile against its `orderId`/`executions` plus `GET /v1/orders/open`, then send a different request if you still need the position. Report the `clOrdId` to operations. |
| A batch ROW (placement, amendment or cancellation, inside HTTP 200) with 5001, 5000 or 5005 | The other rows stand; act on this entry alone. 5001: the entry was not accepted (or not submitted after an earlier entry the intake refused as it was submitted; on a clustered deployment the replicated intake can refuse an entry after the later ones were submitted, which may then take effect, so read every row); resend it, a placement with the same `clOrdId`. 5000: outcome unknown; read the order back on the leader (a placement by its `clOrdId`) and resend a placement only with the SAME `clOrdId`. 5005 (a failure row, or `rejectCode` 5005 on an order row): reconcile, never resend it as it was (section 10). |
| Transport timeout / connection dropped after send | INDETERMINATE, see below. |
| 1004 OTR block | Stop sending new orders, batches and amendments; cancels always pass. Clears as the window slides (section 12). |
| 3003 halted / 3005 no mark yet / 5002 venue halted | Not a retry matter: wait for the state change (`venueEvents`, `GET /v1/status`). |
| 3006 delisted | Permanent for that listing: stop trading it. After a re-listing (section 3) the same symbol can return as a new market with its own grid, under the symbol and `isin` the announcement names: re-read `GET /v1/instruments` after the announced date. |
| 4001 version conflict | Re-read the current config version, then retry. |
| 2003 on an RFQ accept | The quote went stale against the CURRENT mark: take a FRESH quote, never resubmit the old accept. |
| 3007 on an RFQ accept | The QUOTING member no longer passes margin; the quote stays listed. Accept a DIFFERENT quote. |
| 2011 position cap | NEVER retry blindly: your worst-case position valued at the CURRENT mark exceeds `maxPositionNotional` (or, on markets without it, `maxPositionQty`). Reduce exposure or wait for capacity (a cancel of same-direction working orders, a close, or mark drift back under the cap); an unchanged resend rejects again until then. Reduce-only flow always passes (section 3). |
| 2013 order-size cap | NEVER retry the same order: its `price x qty` at the effective price (limit price for priced orders, current mark for market orders) exceeds `maxOrderNotional` (or, without it, `maxOrderQty`). SPLIT it into several orders, each under the cap, each with its OWN fresh `clOrdId`; this applies to `reduceOnly` closes too and to every batch entry independently (section 10). |
| 1001 auth, 1005 scope, other 2xxx validation, 3001/3002/3004 | No retry. Fix the cause. |

**Indeterminate outcomes (timeouts and 5000 on order entry).** A timeout does
NOT mean the order was not accepted; on clustered deployments a write can also
time out venue-side and still commit afterwards. Two safe resolutions:

1. Resubmit the identical payload with the SAME `clOrdId`. The venue answers
   `duplicate: true` if the first attempt landed, or accepts it fresh if it
   did not. Either way you end in a known state. This is the canonical path.
2. If you no longer want the order, reconcile instead: `DELETE
   /v1/orders?clOrdId=<ref>`, then check `GET /v1/orders/history` and
   `GET /v1/executions` for that reference before concluding anything.

**NEVER resubmit with a NEW `clOrdId` while the previous attempt's outcome is
unknown.** That is the classic way to double a position.

**During a leader change** (clustered deployments, the sandbox among them;
member API section 10.7) only a `5030` proves that the command did not take
effect. A single write answered `5001`, a `5000` (REST, or the WebSocket ack
since member API revision 3.83), a batch row `5000` "outcome unknown" and an op
with no ack after the WebSocket `1001` `leader change, reconnect` close are all
indeterminate in the sense above: resolve them on the leader by `clOrdId`. On
FIX the same holds for an order in flight at the leader-change Logout, and for
an order, cancel or replace whose commit is not confirmed within 10 seconds:
that ends the session without a Logout and without a report, and unlike the
leader-change Logout the close counts as a disconnect for an armed cancel on
disconnect, so log on again at once (FIX rules of engagement section 1). A
leader change pulls no resting order and voids every cancel-on-disconnect
arming: re-arm when you reconnect, then re-quote or cancel what you no longer
want.

## 6. Trader IDs (Authorised Users)

- `reg.traderId` is an optional string inside the `reg` object on order entry:
  the venue-issued Trader ID of the Authorised User entering the order. Venue
  operations registers your Authorised Users and issues the ids (default form
  `TR-{memberId}-{8 hex}`, for example `TR-12-9F3A21BC`); treat the value as
  opaque, up to 64 chars.
- Enforcement is STAGED per member. While your member has no registered
  Authorised Users, nothing is gated and existing integrations keep working.
  From the first registered user onward, EVERY order must carry a registered
  ACTIVE Trader ID or it rejects with 2021 (HTTP 400, msg "reg.traderId missing
  or not a registered Authorised User"), raised at the gateway before the
  engine is reached.
- The rule is identical on REST, WebSocket and FIX (one shared code path). On
  FIX the id rides venue tag 9012 on NewOrderSingle. Margin previews are exempt
  on every transport (a preview is a read, not an order entry). RFQ create,
  quote and accept are gated exactly like order entry (section 11).

## 7. What to build (component plan with acceptance criteria)

Generate these components; each has a concrete acceptance test.

1. **Signer.** Pure function producing the three headers.
   Accept: reproduces both section 4 vectors byte for byte.
2. **REST client.** Typed wrappers for the endpoint table in section 8, envelope
   handling, decimal-string parsing, and the section 5 retry matrix: retry only
   idempotent reads and 5001 (backoff + jitter); on 1003 sleep `retryAfterMs`
   (up to 60 seconds after repeated `5005` answers);
   on 5030 resubmit the same `clOrdId` immediately on your configured entry
   host, which the returned `leader` must equal (rule 9 of section 2); NEVER
   blind-retry order placement without reusing the same `clOrdId`.
   Accept: places and cancels a far-from-market LIMIT GTC on sandbox; the cancel
   response echoes `clOrdId` and `clientUid`.
3. **WebSocket market-data client.** `/ws/public`: subscribe `orderbook.25.{sym}`
   (snapshot then deltas, verify the CRC32C `checksum` on every update),
   `trades.{sym}`, `ticker.{sym}`, `markIndex.{sym}`, `funding.{sym}`,
   `venueEvents`.
   Accept: maintains a book whose checksum matches for 5 minutes; surfaces
   `STOP_TRADING`/`RESUME_TRADING` with their `reason`.
4. **WebSocket private client with recovery.** `/ws/private`: auth, subscribe
   `order`, `execution`, `wallet`, `position`, `clientPosition`, `margin`,
   `adl`, `unwind`, `venueEvents` (plus `rfq` and `barriers` when you trade
   RFQ markets; those two recover by state re-read, not cursor backfill, see
   sections 11 and 18). Respond to `ping`; expect server heartbeats.
   Handle the delivery contract: guaranteed channels are never silently dropped;
   if you are too slow the server sends a terminal `gap` frame naming the
   backfillable families and closes with code 4008; idle sockets close 4009.
   On ANY disconnect run the section 14 recovery procedure.
   Accept: kill the socket mid-stream in a test; after recovery the merged event
   log has no gaps and no duplicates.
5. **Order state machine** per section 15, keyed by your `clOrdId` plus the
   venue `orderId`, applying events idempotently so a replayed backfill is
   harmless, and absorbing UNSOLICITED venue-initiated activity keyed on
   `refSource: "VENUE"`.
   Accept: replays a recorded event stream into the same final state twice.
6. **Sub-position ledger (per end client)** built exactly per section 13:
   position arithmetic from the guaranteed `execution` stream only, absolute
   baselines seeded from the position reads at quiescent moments,
   `position`/`clientPosition` used as divergence hints only, and a daily deep
   check against the venue EOD reconciliation endpoint.
   Accept: after a day of simulated trading, your per-client table matches the
   venue's EOD snapshot exactly.
7. **Risk gate (your side).** Before submitting: enforce YOUR per-client
   leverage/exposure limits; check the instrument margin parameters and your
   `imMultiplier` at startup and alarm if the venue's figures disagree with
   your assumptions (or assert them per order with `leverage`/`quoteCcy` and
   treat 2018 as a config alarm).
8. **FIX market-data consumer (only if you take the FIX path).** Build it to
   section 19: one request per symbol keyed on your `MDReqID`, a price-keyed
   book per symbol seeded from the snapshot and maintained by the incremental
   stream, ANY later snapshot treated as a re-baseline (discard and rebuild,
   never merge), and a tape consumer keyed on the venue trade id so prints
   dedupe against `GET /v1/trades`. Message bodies field by field are in
   `KronosX-Claude-Code-FIX.md` section 9.
   Accept: at a quiet moment the maintained book equals
   `GET /v1/orderbook?symbol=&depth=` at the same depth; a forced re-baseline
   rebuilds the book instead of merging into it.

## 8. REST endpoint summary

Public (no auth): `GET /v1/time`, `/v1/status`, `/v1/instruments`,
`/v1/orderbook?symbol=&depth=`, `/v1/trades?symbol=&limit=`,
`/v1/ticker?symbol=`, `/v1/index-price?symbol=`, `/v1/mark-price?symbol=`,
`/v1/funding/current?symbol=`, `/v1/funding/history?symbol=&from=&to=&limit=&includeSkips=`,
`/v1/insurance-fund`.

Private (signed):

| Endpoint | Purpose |
|---|---|
| `POST /v1/orders` | Place order (body in section 9) |
| `POST /v1/orders/batch` | 1 to 50 complete, independent place-order bodies in one message (full contract in section 10) |
| `POST /v1/orders/batch/amend` | 1 to 50 amendments `{orderId, price?, qty?}` of your own orders in one message, each processed exactly as `PUT /v1/orders/{orderId}` (member API 3.74, section 10) |
| `POST /v1/orders/batch/cancel` | 1 to 50 cancellations `{orderId}` or `{clOrdId}` of your own orders in one message, free and never throttled (member API 3.74, section 10) |
| `DELETE /v1/orders/{orderId}` | Cancel by venue id (an empty id, `/v1/orders/` with the trailing slash: 5003, nothing cancelled, member API 3.84); answers the cancelled order, rendered from the cancel event (`acceptedTs` the cancel's time) |
| `DELETE /v1/orders?clOrdId=` | Cancel by your reference, `clOrdId` alone (with `symbol` or `side`: 5003) |
| `DELETE /v1/orders?symbol=&side=` | Cancel all (optionally filtered by `symbol` and `side`, `BUY` or `SELL` in any case; without a filter every open order, untriggered conditionals included; `/v1/orders` without the trailing slash only). Since member API 3.84 the route reads these three parameters and nothing else: another query parameter, one given twice or another `side` is 5003, a request body 2017, and nothing is cancelled. No `clientUid` filter: cancel one end client's orders by `orderId` (`POST /v1/orders/batch/cancel`) |
| `PUT /v1/orders/{orderId}` | Amend price and/or qty (qty-down at the same price keeps priority; price change or qty-up is cancel+new); answers the order read just after the amendment, under the amendment's own `asOfSeq` |
| `GET /v1/orders/{orderId}` | Order state (working or terminal) |
| `GET /v1/orders/open?symbol=` | Open orders (including untriggered conditionals) |
| `GET /v1/orders/history?symbol=&from=&to=&limit=` | Closed orders AND rejections (with `rejectCode`); `from` and `to` bound `acceptedTs` (section 15) |
| `GET /v1/executions?symbol=&from=&to=&limit=` | Fills incl `refSource`, `clOrdId`; held below a store hole while the store reads `REPAIRING` (section 14, member API revision 3.83) |
| `GET /v1/account` | Balance, equity, IM/MM, available, `imMultiplier`, asset holdings |
| `GET /v1/positions` | Member-level positions |
| `GET /v1/positions/end-clients?symbol=&clientUid=` | Your per-`clientUid` sub-positions |
| `GET /v1/account/ledger?kind=&from=&to=&limit=` | Cash movements; each row names its `ccy` after `kind` (Member API 3.70), the same value the WS `wallet` frame carries; held below a store hole while the store reads `REPAIRING` (member API revision 3.83) |
| `POST /v1/margin/preview` (body: hypothetical orders, priced as one basket) | Pre-trade margin check |
| `GET /v1/margin/calls?from=&to=&limit=` | Margin-call history |
| `GET /v1/adl/indicator?symbol=` | Your ADL quintile (1 to 5, 5 first to be deleveraged) |
| `GET /v1/adl/events?from=&to=&limit=` | ADL executions affecting you |
| `GET /v1/unwinds` | Liquidation episode reports |
| `GET /v1/stream/cursor` | Backfill head cursor + store status |
| `GET /v1/stream/events?afterCursor=&limit=` (or `fromSeq=`) | Seq-ordered event backfill (section 14) |
| `GET /v1/reconciliation/end-client-positions?date=` | EOD per-client snapshot (`&format=csv`) |
| `GET /v1/market-segments` / `PUT /v1/market-segments` | Counterparty acceptance (liquidity pools) |
| `PUT /v1/clients/{clientUid}/identity` | RTS 22 identity record per `clientUid` (member reference section 6d): upsert with immutable audit `revision`, strict body (2017), validation reject 2030 naming the field, trading-capable credential required (1005 on READ_ONLY). Never blocks trading |
| `GET /v1/clients/{clientUid}/identity` | The current record; 2031 (404) when none |
| `GET /v1/clients/identity/missing?from=&to=&limit=` | Your uncovered `clientUid`s that traded on MiFIR-scope markets (perpetual and barrier, revision 3.52) in the window: the T+1 chase list (a MiFID II firm member always receives an empty list, revision 3.70). 5004/503 on a deployment without the venue's history store, like the stream routes |
| `PUT /v1/trader-ids/{traderId}/identity` | Revision 3.53: the person (`kind: "PERSON"` + `branchCountry`) or algorithm (`kind: "ALGO"`, `algoId`) behind a Trader ID (RTS 22 fields 57 to 60); strict body (2017), 2030 on validation, 1005 on READ_ONLY. Echoes carry `mifirIdScheme` and, in the sandbox WARN stage until 2026-09-21, a `warnings` array for Annex II contradictions; from Tuesday 2026-09-22 on the sandbox and from the first day on production (ENFORCE) the same findings reject with 2030 |
| `GET /v1/trader-ids/{traderId}/identity` | The current trader identity; 2031 (404) when none |

**Response-shape trap:** `GET /v1/account` returns an ARRAY, one row per
settlement currency. Access `data[0].balance`, not `data.balance`. The
maintenance-margin key on it is `MM` (uppercase), unlike the other camelCase
fields.

RFQ-mode markets only (summary; the full request/response contract lives in
`KronosX-Claude-Code-REST.md`):

| Endpoint | Purpose |
|---|---|
| `POST /v1/rfq` | Create a request for quotes (barrier terms ride here) |
| `GET /v1/rfq/open` | Board of RFQs you may quote (eligibility-filtered, requester anonymous) |
| `GET /v1/rfq/mine` | Your own open RFQs with their quotes (makers anonymous) |
| `GET /v1/rfq/quotes/mine` | Your live quotes across all open RFQs |
| `POST /v1/rfq/{rfqId}/quotes` | Quote an RFQ (one live quote per maker; re-quote replaces) |
| `POST /v1/rfq/{rfqId}/accept` | Accept a quote (executes at the quoted price) |
| `POST /v1/rfq/{rfqId}/cancel` | Cancel your RFQ (never rate-limited) |
| `POST /v1/rfq/{rfqId}/quotes/{quoteId}/cancel` | Cancel your quote (never rate-limited) |
| `GET /v1/barriers` | Your live barrier contracts, both roles, marked live |

Rate limits and the order-to-trade regime are section 12.

## 9. Order placement body

```json
POST /v1/orders
{
  "symbol": "BTC-USD-PERP",
  "side": "BUY",                       // BUY | SELL
  "type": "LIMIT",                     // LIMIT | MARKET | STOP_MARKET | STOP_LIMIT | TAKE_PROFIT_MARKET | TAKE_PROFIT_LIMIT
  "tif": "GTC",                        // GTC | IOC | FOK | POST_ONLY | GTD
  "expireTs": 1785999999000,           // GTD only, unix ms
  "price": "65000",                    // LIMIT only; omit for MARKET
  "qty": "0.25",                       // decimal base units on the qtyStep grid (section 3)
  "reduceOnly": false,                 // perps only; 2016 on spot
  "clOrdId": "a3f1c9d24b7e4f108c6d2e5b90a1",
  "clientUid": "CLIENT-A",
  "leverage": 10,                      // OPTIONAL assertion, see rule 7
  "quoteCcy": "USD",                   // OPTIONAL assertion
  "trigger": {                         // conditionals only
    "source": "MARK",                  // LAST | MARK | INDEX (prefer MARK for stops: LAST can be moved by one small print)
    "price": "70000",
    "direction": "AT_OR_ABOVE",        // AT_OR_ABOVE | AT_OR_BELOW
    "limitPrice": "70200"              // *_LIMIT types only
  },
  "reg": { "traderId": "TR-12-9F3A21BC" } // optional MiFID RTS 24/22 fields + Trader ID (section 6), ask venue ops
}
```

Semantics worth coding to:
- Order entry is valid on `mode: "ORDER_BOOK"` instruments only; an RFQ-mode
  market rejects with 2022 (trade it via the RFQ surface, section 11).
- MARKET orders execute inside a protection collar
  (`marketMaxSlippageBps` off the best eligible price); margin is checked at
  the collar price, so a market buy needs slightly more free margin than the
  touch suggests; an empty book rejects with 2007. MARKET+FOK is all-or-none
  against liquidity INSIDE the collar: either one full fill set or a single
  cancel with reason `FOK_UNFILLABLE`, never a partial.
- IOC remainder cancels with reason `IOC_REMAINDER`. POST_ONLY that would cross
  rejects with 2005. GTD expires with reason `GTD_EXPIRED`.
- Untriggered conditionals rest in status `PENDING_TRIGGER` (section 15).
- Spot symbols (`kind: "SPOT"`) are fully prefunded: buys need quote balance,
  sells need base-asset balance (3004); `reduceOnly` and market-conditional
  BUYS reject with 2016; a resting spot order that loses its funding cancels at
  fill time with reason `SPOT_UNFUNDED`.
- Self-trade prevention is scoped to the END CLIENT by default: your two
  different `clientUid`s can trade with each other (legitimate agency crossing);
  the same `clientUid` never self-trades (your configured STP policy applies).

## 10. Batch order entry (mass close with per-client attribution)

`POST /v1/orders/batch` submits **1 to 50 complete, independent orders in one
signed message**; the WebSocket twin is op `orders.batch` with the same body in
`args[0]`. It is NOT offered over FIX: FIX members loop NewOrderSingle
messages instead. Every entry is a full section 9 body with its **own
`clOrdId` and its own `clientUid`**; that is the point of the feature: one
message, N client-tagged orders. The canonical use case is the mass close, one
`reduceOnly` close per end client when you unwind a book:

```json
POST /v1/orders/batch
{
  "orders": [
    { "symbol": "BTC-USD-PERP", "side": "SELL", "type": "LIMIT", "tif": "GTC",
      "price": "65000", "qty": "0.2", "reduceOnly": true,
      "clOrdId": "close-cl-a-1", "clientUid": "CLIENT-A" },
    { "symbol": "BTC-USD-PERP", "side": "SELL", "type": "LIMIT", "tif": "GTC",
      "price": "65000", "qty": "0.5", "reduceOnly": true,
      "clOrdId": "close-cl-b-1", "clientUid": "CLIENT-B" },
    { "symbol": "BTC-USD-PERP", "side": "SELL", "type": "LIMIT", "tif": "GTC",
      "price": "65000", "qty": "0.1", "reduceOnly": true,
      "clOrdId": "close-cl-c-1", "clientUid": "CLIENT-C" }
  ]
}
```

Contract:

- **Non-atomic by design.** The batch is a transport optimisation, not a
  transaction. Each entry validates and executes independently, exactly as if
  sent as a single `POST /v1/orders` (strict fields 2017, reserved `KX-`
  namespace 2020, Trader ID 2021, `reduceOnly` semantics, the `minNotional`
  floor 2028 with its reduce-only exemption, per-entry idempotency on its
  `clOrdId`). The section 3 caps run PER ENTRY at each entry's own effective
  price: an entry over `maxOrderNotional` rejects **2013** on that entry
  alone (`reduceOnly` entries included; split the oversized entry into
  several smaller entries), and entries admit against the position cap
  (**2011**) in array order, so a later entry can reject because earlier
  entries in the same batch consumed the remaining capacity. One entry's
  rejection never affects its neighbours; there is no all-or-nothing
  execution mode.
- **Sequencing.** Entries are submitted to the matching engine in array order,
  and array order is the engine sequencing order among the entries of your
  batch. Two crossing entries in one batch behave deterministically (the
  earlier rests, the later matches it). Other members' traffic may interleave
  between your entries; a batch holds no lock on the book.
- **Response.** HTTP 200 with envelope `code` 0 and `data.results`, one entry
  per order in array order, even when every entry failed. A successful entry
  has exactly the shape of the single-order place response data (final status
  plus any executions produced while matching; engine-level rejects such as
  2028 ride that payload as `rejectCode` with `status` `REJECTED`). A
  failure entry is `{"code": ..., "msg": ..., "clOrdId": ...}`, echoing that
  entry's `clOrdId` even when the entry was malformed: a gateway refusal, the
  matching engine's 2009, 5001 for an entry the venue did not accept (after an
  entry the intake refused as it was submitted, every later entry answers 5001
  "not submitted"; on a clustered deployment the replicated intake can also
  refuse an entry after the later ones were submitted, and those may take
  effect: read every row) or, since member API 3.74, 5000 for an entry whose outcome the venue could
  not confirm (reconcile it by its `clOrdId` and resend it only with the SAME
  `clOrdId`; before 3.74 such a fault answered the whole request with an
  undocumented HTTP 500). Entry member names are case-sensitive (`Qty` is an
  unknown field, 2017), where the single `POST /v1/orders` body is not.
- **Envelope-level errors** are limited to authentication (1001), the
  read-only credential reject (1005), throttle (1003), a malformed body or an
  entry count outside 1 to 50 (2002 with a descriptive `msg`), the engine-busy
  shed (5001), the order-to-trade block (1004) and 5030 with the leader on a
  clustered deployment's follower node (REST); the 5001 and the 1004 come after
  the charge, so the N units stay spent.
- **Rate limits.** The batch consumes one order-budget unit PER ENTRY, charged
  up front, all or nothing: if fewer than N units are available the whole
  batch answers 1003 with `retryAfterMs` covering the full deficit and
  consumes nothing. Every entry counts as one order message under the
  order-to-trade regime (section 12), whatever its per-entry outcome.

Worked response for the request above, with 0.2 resting on the bid at 65000
(array order means the first entry takes that bid and fills while the second
and third rest working):

```json
{
  "code": 0, "msg": "ok",
  "data": {
    "results": [
      { "orderId": 887731, "clOrdId": "close-cl-a-1", "symbol": "BTC-USD-PERP",
        "side": "SELL", "type": "LIMIT", "tif": "GTC", "price": "65000",
        "qty": "0.2", "remainingQty": "0", "reduceOnly": true,
        "clientUid": "CLIENT-A", "status": "FILLED",
        "executions": [
          { "tradeId": 55131, "orderId": 887731, "symbol": "BTC-USD-PERP",
            "side": "SELL", "price": "65000", "qty": "0.2", "fee": "6.5",
            "role": "TAKER", "flags": "NORMAL", "clOrdId": "close-cl-a-1",
            "refSource": "MEMBER", "clientUid": "CLIENT-A",
            "ts": 1785000000117 } ],
        "acceptedTs": 1785000000117 },
      { "orderId": 887732, "clOrdId": "close-cl-b-1", "symbol": "BTC-USD-PERP",
        "side": "SELL", "type": "LIMIT", "tif": "GTC", "price": "65000",
        "qty": "0.5", "remainingQty": "0.5", "reduceOnly": true,
        "clientUid": "CLIENT-B", "status": "NEW", "acceptedTs": 1785000000117 },
      { "orderId": 887733, "clOrdId": "close-cl-c-1", "symbol": "BTC-USD-PERP",
        "side": "SELL", "type": "LIMIT", "tif": "GTC", "price": "65000",
        "qty": "0.1", "remainingQty": "0.1", "reduceOnly": true,
        "clientUid": "CLIENT-C", "status": "NEW", "acceptedTs": 1785000000117 }
    ]
  },
  "asOfSeq": 104874, "ts": 1785000000119
}
```

Had one entry failed gateway validation instead, only that row would change,
for example `{"code": 2002, "msg": "price not on the tick grid, or qty not an
exact multiple of qtyStep", "clOrdId": "close-cl-c-1"}`; its neighbours stand,
and you resend only the corrected entry.

On WebSocket, `orders.batch` acks with the same `data.results` on the
guaranteed reply channel; pipelining order ops (sending without waiting for
each ack) composes with batching and is the biggest throughput lever for a
DMA integration.

### 10.1 Batch amendment and batch cancellation (member API 3.74)

Two more routes carry 1 to 50 entries in one signed message, for a market
maker that requotes many levels: `POST /v1/orders/batch/amend` (WS op
`orders.batchAmend`) and `POST /v1/orders/batch/cancel` (WS op
`orders.batchCancel`). Neither is offered over FIX: a FIX member loops 35=G or
35=F, or uses these routes on a native connection alongside.

```json
POST /v1/orders/batch/amend
{ "amends": [
    { "orderId": 887731, "qty": "0.15" },
    { "orderId": 887732, "price": "64990.5" } ] }

POST /v1/orders/batch/cancel
{ "cancels": [
    { "orderId": 887734 },
    { "clOrdId": "q-ask-3" } ] }
```

- **The engine sees the single route.** Every amendment entry the venue
  submits is processed exactly as `PUT /v1/orders/{orderId}` (`qty` the new
  TOTAL quantity; a quantity decrease at the same price keeps priority, a
  price change or a quantity increase is cancel+new; every engine check of
  the single amendment applies at the current state), every cancellation entry
  exactly as `DELETE /v1/orders/{orderId}`. An amendment entry names its order
  by `orderId` only; a cancellation entry by `orderId` OR `clOrdId`, exactly
  one. Entry member names are case-sensitive and a wrong JSON type is 2017. An
  order named by two entries: the first runs, every later one answers 2013.
- **Non-atomic by design.** Entries reach the engine in array order, other
  members' traffic may interleave, and a refused entry never stops a later
  one. Send an entry that depends on another's outcome in a SECOND message
  after reading the first message's rows. When you shift a two-sided quote,
  amend the side moving away from the other first (in a shift up the asks
  before the bids) and size down before you size up; the advice holds only
  when every earlier entry takes effect.
- **Rows.** HTTP 200, envelope `code` 0, `data.results` in array order and
  `asOfSeq`, even when every entry failed. A row carrying `code` is a failure
  row (`{code, msg, orderId?, clOrdId?}`, echoing what the entry carried);
  any other row is the order (the single route's `data`; for an amendment,
  when the venue could not read your orders back after the batch, the order's
  last state change among the batch's entries, with `asOfSeq` the lowest seqNo
  at which a still-working row was taken, or the seqNo before it when the
  command that made that row's change also traded the order (the row is then
  the order before those fills), so every row is the order as of `asOfSeq` or
  later and the `order` and `execution` events after it bring it up to date). A success row of an amendment can read `REJECTED` (2005, a post-only order repriced to cross;
  2015 under `REJECT_TAKER`) or `CANCELED` (`STP_CANCEL`): that level is gone,
  place it again with a placement batch.
- **Cancellations are looked up first and free.** An entry naming no open
  order of yours answers 2014 and nothing is submitted for it. No
  order-budget unit, never throttled, never blocked by the order-to-trade
  regime. One cancellation batch of yours is processed at a time on a venue
  node, a later one waits for it; 5001 when eight already wait or the intake
  is nearly full. Keep a SECOND private socket for urgent cancels: a
  WebSocket session processes one message at a time.
- **Amendments are charged.** One order-budget unit per entry, up front, all
  or nothing, after the engine-busy check; the units of entries refused after
  the charge (2014, 1004, 2049, grid) stay spent. An entry on a market where
  the order-to-trade regime blocks you answers a 1004 row and the others run;
  an entry counts one order message when the venue hands it to the engine.
  Send only the levels that changed: a no-op entry costs a unit like any
  other.
- **Envelope errors** come only before anything is submitted: 1001, 1005,
  2002, 5001 (shed, lookup, the cancellation queue), 5030 with the leader on
  a follower node (REST and both WS ops), and for the amendment batch 2034
  (production) and 1003. The WS acks of the two ops carry `asOfSeq` at the top
  level.
- **Unsettled rows.** 5001: not accepted, resend that entry; after an entry
  the intake refused as it was submitted the later ones answer 5001 "not
  submitted", but on a clustered deployment the replicated intake can refuse
  an entry after the later ones were submitted, which may then take effect:
  read every row, never infer the rest from the first 5001. 5000: outcome
  unknown, read the order back on the leader first. 5005: reconcile, never
  resend as it was. A refused RESEND of an amendment does not mean the first
  attempt failed (the engine re-checks band, margin and session at the
  current state): read `GET /v1/orders/open` before acting.
- **Withdrawal and re-certification.** The venue may withdraw either route
  during an incident (it then answers 404, WS 2013 "unknown op", as before
  3.74; your single routes stay). Adopting either route is a material update
  of your trading system: re-certify with the Conformance Test Plan's rows
  X-07 to X-11 before production use.

## 11. RFQ markets and barrier contracts (summary)

Some instruments trade by request-for-quote instead of a central book
(`mode: "RFQ"` on `/v1/instruments`; barrier markets are always RFQ). This
section is the working summary: the full endpoint and event contract lives in
`KronosX-Claude-Code-REST.md` and `KronosX-Claude-Code-WebSocket.md`. RFQ is
REST/WS only; it is NOT offered over FIX.

**Flow.** The requester posts `POST /v1/rfq` with `{symbol, side, qty,
expiresInMs?, barrier?, clOrdId?, clientUid, reg?}` (quoting window default
60000 ms, min 1 s, max 24 h, 2013 outside). Makers discover it on
`GET /v1/rfq/open` and respond with `POST /v1/rfq/{id}/quotes` carrying
`{price, expiresInMs?, clOrdId?, clientUid, reg?}` (quote validity default
30000 ms); the requester accepts one with `POST /v1/rfq/{id}/accept`
`{quoteId, reg?}` and receives `{rfqId, quoteId, tradeId, symbol, side, price,
qty, contractId?, ts}`: one trade at the quoted price for the request's FULL
size. The Trader ID gate (2021, section 6) applies to create, quote and
accept; the reserved `KX-` clOrdId prefix rejects with 2020 on create and
quote. The `minNotional` floor applies here too (section 3): a create below
the floor valued at the mark rejects 2028, and every quote is re-checked at
its quoted entry price. On the barrier markets the floor is 100 USD (member
API revision 3.71) and it also applies to a close request at the contract's
whole quantity valued at the mark, so a contract worth less than that ends at
its barrier, its take profit or its expiry, not by an early close; size an
opening request with headroom because a quote below the mark is re-valued at
its own price.

**Validation to code against.** A quote outside the instrument's price band vs
mark rejects with 2003, and the band is RE-APPLIED at accept against the
CURRENT mark, so accepting a stale quote also rejects 2003 (take a fresh
quote). Self-quote and self-accept reject with 2026. One live quote per maker
per RFQ (a re-quote replaces the old one); at most 64 live quotes per RFQ and
64 open RFQs per member (2012). A quote commits the maker to the request's
FULL size against its order-size cap (2013): `maxOrderNotional` valued at the
quoted entry price where published, else the base-unit `maxOrderQty` (the
request itself was already checked at the mark on create, section 3). Both
parties are margin-checked
at accept: requester failure rejects 3001 (3004 on the spot-sell asset leg),
maker failure rejects 3007 and the quote stays listed (accept another). 2024 =
RFQ not found, closed, expired or not visible to you (HTTP 404); 2025 = quote
not found, withdrawn or expired (404). A reduce-only requester rejects with
3002; a reduce-only maker's quote resolves as 2025.

**Executions.** A perp RFQ trade books positions exactly like a book fill and
publishes on the trades feed and your `execution` channel flagged `"RFQ"`, but
it never moves the last-trade print, never fires conditional triggers and
never feeds the circuit breaker. A spot RFQ trade is prefunded delivery at the
quoted price.

**Barrier contracts** (`kind: "BARRIER"`). The requester is ALWAYS the holder:
BUY = long, knocked out at or below the barrier, which sits BELOW entry;
SELL = short, barrier ABOVE. The maker writes the contract and quotes the
ENTRY price; `barrier: {level, expiryTs?, takeProfit?, strictTakeProfit?}`
rides the RFQ, and invalid terms reject with 2027 (the optional `takeProfit`,
revision 3.43, is a second exit on the PROFIT side of the entry at which the
contract also terminates AT the level, fee-free; the writer's margin is then
bounded by the remaining take-profit distance). Holder margin is the PREMIUM
(revision 3.45): the entry-to-barrier distance times qty times the contract's
`premiumBps` share, frozen at the open (`10000 - knockOutRebateBps` of the
instrument on a normal contract, 9000 = 90 percent on every KO market listed
today; `strictTakeProfitPremiumBps` on a strict one, 5000 today; both fields
on `GET /v1/instruments` for BARRIER markets), with IM equal to MM: the exact
maximum loss. It then tracks the REMAINING mark-to-barrier distance less the
rebate, so the contract can never liquidate its holder and profits stay
reserved until close. Knock-out settles AT the barrier (a guaranteed stop, no
gap-through) and costs the holder exactly the premium; a take profit settles
AT the take-profit level the same way; expiry settles at mark on the first
tick at or past `expiryTs`, floored at minus the premium and capped at the
potential profit. `barrier.strictTakeProfit: true` (needs `takeProfit`, an
`expiryTs` within 24 h and a market whose strict share is nonzero, else 2027)
buys the cheaper STRICT variant that pays only on a take-profit touch before
expiry and forfeits the whole premium at expiry otherwise. Every barrier view
(RFQ create response, `mine`, `open`, WS `rfq` `created`, the accept
response, `GET /v1/barriers` rows, WS `barriers` frames) carries
`strictTakeProfit`, `premium` (indicative at the current mark on an open
request, frozen elsewhere) and `premiumBps`.
Venue settles are fee-free; entry pays normal maker/taker fees on the entry
notional. Settlement cash arrives as ledger kind `BARRIERSETTLE`.
Contracts never appear in `/v1/positions` or open interest: read them from
`GET /v1/barriers` (role `HOLDER`|`WRITER`, direction, qty, entry, barrier,
takeProfit?, strictTakeProfit, premium, premiumBps, markPrice,
barrierDistance, unrealizedPnl, margin, expiryTs?, clientUid, openedTs). Writers margin like a perp on the risk tiers; a member entering
liquidation has its contracts torn up at the barrier-clamped mark, and a perp
position on an RFQ market is liquidated AT MARK against opposite-side holders
(it arrives as an ADL-flagged trade, no bankruptcy price required).

**WebSocket.** Private channels `rfq` (types `created`, `quoted`, `myQuote`,
`quoteCanceled`, `accepted` with your `role`, `canceled`, `expired`) and
`barriers` (`opened`, `knockOut`, `expiry`, `liquidation`, `finalSettlement`,
closes carrying `settlePrice` and role-signed `pnl`). Both are GUARANTEED but
NOT cursor-backfillable: after a `gap` frame re-read `/v1/rfq/mine`,
`/v1/rfq/open` and `/v1/barriers` instead (state-only recovery). The sandbox
barrier market is `BTC-USD-KO`, with its own mark feed
(`markIndex.BTC-USD-KO`).

## 12. Rate limits and the order-to-trade regime

Two INDEPENDENT continuous-refill token buckets per MEMBER, shared by every API
key the member holds (defaults; venue operations can tune per member):

| Bucket | Default | Consumed by |
|---|---|---|
| Read | 6000/min | Every private GET, WS `get.*` reads, the RFQ and barrier GET routes |
| Order | 40000/min; per-second ceiling per class beside the per-minute rate, 1,000/s STANDARD on production, 0 on the sandbox | Order placements, EVERY placement or amendment batch entry (one unit each), amendments, RFQ create, quote and accept, on every transport; cancellation batches never |

Beside the read bucket, the reads of the venue's history store carry the
bounds of member reference section 4 since revision 3.73 (the history-read
bullet below): a bound on your reads of each group in flight per node, a budget
of these reads per node that every member shares, a time bound, a hold and a
store yield.

- **Cancels consume neither bucket and are never throttled** (order cancels,
  cancel-all, RFQ cancels, quote cancels): a member must always be able to
  withdraw a price. Order-budget traffic does not consume read tokens, so
  heavy order flow never starves your polling and vice versa.
- Both buckets are full-capacity: after a quiet period the whole minute's
  allowance is available at once (sized for DMA liquidation waves, where a
  burst of risk-reducing orders must not be throttled), within the per-second
  ceiling your class publishes (STANDARD 1,000 orders/s on production, none on
  the sandbox; member reference section 4): a burst above it answers 1003 with
  `retryAfterMs` under one second even while the minute's allowance is unspent.
- **1003 handling.** A request a bucket denies answers HTTP 429, `code` 1003,
  with the EXACT wait in `data.retryAfterMs` (also on the
  `X-RateLimit-Retry-After-Ms` header; a standard `Retry-After` header carries
  it rounded up to seconds). Sleep exactly `retryAfterMs`, then resend the same
  message with the same `clOrdId`. Do not guess or exponential-backoff on top of
  it. A denied batch consumes nothing and its `retryAfterMs` covers the FULL
  N-unit deficit. The history-read 1003 of the next bullet is the exception.
- **1003 on a history read (since member reference revision 3.73).** On the
  routes of the venue's history store (`GET /v1/stream/events`,
  `GET /v1/stream/cursor` and WS `get.streamCursor`, `GET /v1/margin/calls`,
  `GET /v1/executions`, `GET /v1/orders/history`, `GET /v1/account/ledger`,
  `GET /v1/adl/events`, `GET /v1/reconciliation/end-client-positions`) a 1003
  can also come from the bound on your reads of the group in flight on the node
  or from the node's budget that every member shares. Its `retryAfterMs` is
  then a SUGGESTED wait, not an exact refill: sleep it and resend the same read
  (never advance a cursor on a refusal). A repeated 1003 while other reads of
  yours of the group are in flight means your reads overlap: serialise your
  readers. One while no other read of yours of the group is in flight is the
  shared budget's, which serialising cannot help: back off exponentially. The
  time bound, the hold and the store yield answer 5007 (section 5).
- **Order-to-trade regime (RTS 9).** Independently of the buckets, the venue
  counts your order messages against your trades over a sliding 60-minute
  window; the regime only binds after a minimum number of orders, and
  thresholds are communicated when your account is configured. Exceeding the
  block threshold answers 1004 (HTTP 429) on NEW orders, placement batch
  entries (each entry counts as one message, whatever its outcome) and
  amendments only; an amendment batch answers 1004 as the row of each entry on
  a blocked market and runs the others (member API 3.74); cancels always
  pass. The block clears as the window slides; a lower warning
  threshold flags you to venue operations without rejecting.

## 13. Position seeding and reconciliation (read this before writing position code)

This is the section most integrations get wrong, and getting it wrong is
silent until it costs money. Build your position store on this division of
labour:

- **`execution` is the ONLY stream you derive position arithmetic from.** It
  is guaranteed, durable and cursor-backfillable: every fill, including
  venue-initiated liquidation and ADL legs (`refSource: "VENUE"`) and RFQ
  fills (flagged `RFQ`), reaches you exactly once per cursor, live or via the
  section 14 backfill, even across your own downtime.
- **`position` and `clientPosition` are best-effort CHANGE HINTS, never a
  source of truth.** They may be conflated under pressure (announced by a
  non-terminal gap frame), they have no backfill history, and a client that
  integrates deltas from them WILL drift. Use them for display and divergence
  detection only. (`clientPosition` is high volume for a large broker;
  subscribe deliberately.)

**The failure mode this prevents.** A client that derives positions from the
position channels, or that resumes after a restart by trusting its last
in-memory state, diverges SILENTLY: one conflated frame, or one fill executed
while you were down, and every subsequent number is wrong until something
breaks loudly (a reduce-only close rejecting 2008, a margin surprise, an EOD
break). The other classic failure is the phantom mid-flow mismatch: comparing
your ledger against a live read while your own executions are still in flight
manufactures false breaks, because reads and streams are ordered by
`asOfSeq`, not by wall clock.

**Seeding an absolute baseline.** Deltas need an anchor. On every startup,
after every reconnect, and after any gap frame naming `position` or
`clientPosition`:

1. Finish the section 14 catch-up FIRST: page the event backfill until
   `store.status` is `COMPLETE` and `hasMore` is false, and drain your live
   buffer, so no execution you have not applied is still in flight toward you.
2. Pick a QUIESCENT moment: you have no working orders whose fills could land
   mid-read (or accept that the rows are only valid as of the response's
   `asOfSeq`).
3. Read BOTH baselines and adopt them ABSOLUTELY, recording each response's
   `asOfSeq` as the baseline sequence:
   - `GET /v1/positions` (member level)
   - `GET /v1/positions/end-clients` (per `clientUid`)
   Rows with zero net quantity are OMITTED from both reads: treat absence as
   flat, and clear any local row the read did not return.
4. From the baseline forward, advance positions by applying `execution`
   events ONLY (signed by side; venue legs and RFQ fills included), skipping
   any execution whose cursor is at or below the baseline `asOfSeq`.
5. Treat incoming `position`/`clientPosition` frames as cheap divergence
   hints: if a hint disagrees with your ledger AFTER your applied cursor has
   passed that frame's `asOfSeq` and you are quiescent, re-seed (steps 1 to
   3) and raise an operational alert. Never "correct" your ledger from the
   hint directly.

Example baseline rows (shapes as served; member row first, then a
per-client row):

```json
GET /v1/positions          ->  [ { "symbol": "BTC-USD-PERP", "netQty": "0.25",
                                   "avgEntry": "68900", "markPrice": "70016.5",
                                   "unrealizedPnl": "279.125", "realizedPnl": "0",
                                   "accFunding": "-1.24", "adlQuintile": 2 } ]

GET /v1/positions/end-clients -> [ { "symbol": "BTC-USD-PERP", "netQty": "0.2",
                                     "avgEntry": "68900", "markPrice": "70016.5",
                                     "unrealizedPnl": "223.3", "realizedPnl": "0",
                                     "accFunding": "-0.992", "clientUid": "CLIENT-A" },
                                   { "symbol": "BTC-USD-PERP", "netQty": "0.05",
                                     "avgEntry": "68900", "markPrice": "70016.5",
                                     "unrealizedPnl": "55.825", "realizedPnl": "0",
                                     "accFunding": "-0.248", "clientUid": "CLIENT-B" } ]
```

`netQty` is signed (negative = short). The member row is the net of your
client rows; barrier contracts NEVER appear here (they live on
`GET /v1/barriers` only, section 11).

**Reconcile only at quiescence.** Only compare your ledger against a read once
your applied execution cursor has passed the read's `asOfSeq` and nothing of
yours is working. For the authoritative deep check, align with the venue's own
daily cycle: reconcile your per-client table against
`GET /v1/reconciliation/end-client-positions?date=YYYY-MM-DD` (CSV mode with
`&format=csv`; columns `date,clientUid,symbol,positionSide,netQty,avgEntry,mark,upnl`,
`positionSide` telling the hedge-mode buckets of one client apart).
Since member API 3.81 `date` is `YYYY-MM-DD` (the CSV printed it month first
before), a field holding a comma, a double quote, CR or LF is quoted per
RFC 4180, and an absent value is an empty field. `upnl` (member API 3.81) is
the unrealized PnL at `mark` in the market's settlement currency, exact to
the micro: the `unrealizedPnl` `GET /v1/positions/end-clients` reports at that
mark. It is empty where the venue's history store had not caught up with its
matching engine on that row at the snapshot, without a mark, and on rows
recorded before 3.81. The venue takes the snapshot at its daily anchor,
within about five minutes after 00:00 UTC for the day that just ended, while
the venue keeps trading: a fill of yours in those first minutes of the new
day can be in it, so a break on a client that traded then is a timing
difference to check against your fills of the new day, not a missing fill.
If the venue restarts, or another of its nodes takes over, before the next
anchor, the anchor runs again for the previous day and adds the rows of the
sub-positions that day does not hold yet, from the state at that moment,
`mark` and `upnl` included (all of the day's rows when the anchor had not run
at midnight); a snapshot venue operations take by hand does the same, and a
row already written never changes. A row for a client that was flat at
midnight comes from such a later state: check it against your fills of the
new day too. It is the same data the venue itself reconciles against.

**Funding.** The venue applies funding at MEMBER level (ledger kinds
`FUNDINGPAY` and `FUNDINGRECEIVE`, plus `funding.{sym}` settlement events), so
there are no per-client funding ledger rows. What it does publish per client is
the BOOKED lifetime figure of each sub-position, `accFunding` on
`GET /v1/positions/end-clients` and on the `GET /v1/lots` envelope (which still
answers for a client that is flat again): reconcile your own per-client
allocation against it, never against the sum of the lots.

## 14. Recovery: cursor backfill and reconnect

Every event-class message the private socket delivers is also written to a
durable, sequence-ordered event store and can be re-read over REST. This is
how you recover from a disconnect, a slow-consumer close (4008), or your own
downtime without losing a single notification. Even a REST-only integration
should page the backfill from a persisted cursor at startup to prove no
activity was missed.

**The cursor** is 30 zero-padded ASCII digits: `seq` (19) + `eventIndex` (10)
+ `leg` (1); example seq=104871, eventIndex=4, leg=0 is
`"000000000000010487100000000040"`. Ordinal string comparison equals engine
event order in any language. `leg` is 0 except on executions, where leg 0 is
the MAKER leg and leg 1 the TAKER leg (one trade between two of your own end
clients delivers both legs at the same `(asOfSeq, eventIndex)`). Live private
frames carry `asOfSeq` and `eventIndex`; derive the live cursor with leg = 1
for `execution` frames whose `role` is `TAKER`, 0 otherwise. Cursors are
strictly increasing but NOT contiguous for you (other members' events consume
sequence numbers too). The field ranges (member API section 11.2, revision
3.83 states them): `seq` is a signed 64-bit integer, 0 to
9223372036854775807; `eventIndex` a signed 32-bit integer, 0 to 2147483647,
the 0-based position of an event among the events of ONE engine command that
restarts at 0 for every `seq` (not a running counter; ten digits is the padded
width, not the range); `leg` one digit (deliveries use 0 and 1, 9 marks a
bound). A well-formed 30-digit `afterCursor` with a field above its range
answers 2019, since member API revision 3.83 with a `msg` naming the field and
the range; anything that is not 30 ASCII digits answers `afterCursor must be
30 numeric characters`. Branch on the code.

**`GET /v1/stream/events`** (exactly ONE of `afterCursor`/`fromSeq` required):

| Param | Meaning |
|---|---|
| `afterCursor` | 30-char cursor, EXCLUSIVE: strictly greater deliveries |
| `fromSeq` | int64, INCLUSIVE lower bound on `seq` (for clients that kept only `asOfSeq`) |
| `toSeq` | optional inclusive upper bound |
| `channels` | optional CSV. The accepted families are exactly the ten `order`, `execution`, `wallet`, `margin`, `adl`, `unwind`, `eligibility`, `funding`, `status`, `swaps`; naming anything else (including `rfq`, `barriers`, `venueEvents`, `position`) rejects with 2019, and a value that names no family (empty, commas only) with 5003 |
| `limit` | default 500, max 2000 |
| `waitMs` | 0 to 2000: bounded wait for the store to catch up |

The response carries `events[]` (each with `channel`, `eventType`, `asOfSeq`,
`eventIndex`, `leg`, `cursor` and a `data` object byte-identical to the live
frame's `data`), `nextCursor` (on a page with `hasMore` true the cursor of
the LAST returned event; on a page with `hasMore` false a position at the
store watermark the page was read at, or at your `toSeq` when lower, never
below your own cursor and valid only with the same `channels` and `symbol`:
`eventIndex` 2147483647 and `leg` 9, a bound, not a delivery), `hasMore`,
`count` and `store`: `{contiguousThroughSeq, engineSeq, lagSeq, status, complete}`.
Stop a paging loop on `hasMore` false or `count` 0, never on `nextCursor`
equal to the `afterCursor` you sent: the watermark moves between calls while
the venue trades. The venue serves that last-page position on an environment
from the date venue operations announce for it there (member API section
11.3, revision 3.83 states the condition); before that date, and on a page
read while the store recorded or cleared a hole, a last page answers the
cursor of its last event, else the cursor you sent. The loop rule above is
correct under both.
`store.status` is a three-value enum: `COMPLETE` (level with the engine),
`CATCHING_UP` (poll again with `afterCursor=nextCursor`; the store write runs
marginally behind the live socket flush, so a backfill issued microseconds
after a live frame can legitimately come back short), `REPAIRING` (the store
has a recorded hole: reads deliberately STALL below the hole rather than skip
it; keep polling and raise an operational alert).

**`REPAIRING` in detail** (member API section 11.3, revision 3.83 states it).
The `store` block describes the store of the NODE that answered. `REPAIRING`
starts at every leader change of a clustered venue (the sandbox included), when
the store falls further behind than its write buffer absorbs, and when a store
write fails beyond a short retry. No timeout ends it and the venue sets no
maximum duration: it ends when the hole is re-folded from the venue's journal,
by the new leader after a leader change and by venue operations otherwise, a
hole that opens on a running leader included; the older the hole, the longer
the repair. Meanwhile `contiguousThroughSeq` and the envelope `asOfSeq` stop
just below the hole, `lagSeq` counts from the start of the hole to the engine
head (not the size of the hole, so it grows while the hole stands), and
`GET /v1/executions` and `GET /v1/account/ledger` serve only rows at or below
the watermark (since member API revision 3.83; the closed-order rows of
`GET /v1/orders/history` are not held: they can include orders closed after
the hole, above `asOfSeq`, while orders closed inside it are missing). Until
it clears you cannot
backfill past the hole: keep consuming live frames, recover the state you need
from the state reads (open orders, positions, account), and never skip the
hole. Trading and the live feeds do not depend on the store. A node that is not
the leader answers the event stream reads from its own store position, which
no longer advances; while it records no hole, its `GET /v1/executions` and
`GET /v1/account/ledger` read the venue's shared store whole, so their rows can
run ahead of the `asOfSeq` it answers. It closes the connection after its
answer, so your next request reaches the leader.

**`GET /v1/stream/cursor`** (WS twin `get.streamCursor`) returns `headCursor`,
`retentionFromCursor` and the same `store` block. Persist your high-water
cursor durably; get your FIRST cursor from this route at startup. Default
retention is 35 days: if your stored cursor is older than
`retentionFromCursor`, a full backfill is impossible; fall back to snapshot
reads plus the dated history routes and re-seed positions per section 13.

**Gap frames.** If anything is dropped you are told:

- Non-terminal (`reason: "CONFLATED"`, `terminal: false`): market data or a
  best-effort channel conflated; the socket stays open. Recover by re-reading
  state (`resync` for books, snapshot reads for positions), never by cursor.
- Terminal (`reason: "SLOW_CONSUMER"`, `terminal: true`): a guaranteed queue
  overflowed because you stopped reading. The frame names the affected
  families, the inclusive resume cursor (`fromSeq`/`fromEventIndex`), a
  ready-made `backfill` request, and an `unrecoverable` array for guaranteed
  families with no cursor store (`venueEvents`, `rfq`, `barriers`); then the
  socket closes with 4008.

```json
{"channel":"gap","type":"gap","reason":"SLOW_CONSUMER","delivery":"GUARANTEED",
 "terminal":true,"channels":["execution","margin","order","wallet"],
 "fromSeq":904118,"fromEventIndex":2,"inclusive":true,"toSeq":null,"dropped":1287,
 "backfill":{"path":"/v1/stream/events",
   "query":"fromSeq=904118&channels=execution,margin,order,wallet&limit=500"},
 "unrecoverable":null,"ts":1785312044518}
```

**Reconnect procedure (mandatory, in this order):**

1. While connected, track the highest cursor per guaranteed channel; persist
   the cursor of every event you finish processing and, per backfill filter,
   the `nextCursor` of your last page.
2. On a terminal gap frame keep its cursor and `unrecoverable` list; on any
   close without one (network drop, 4009) use your RESUME POINT: per filter,
   the ordinal maximum of the `nextCursor` of your last page and the cursor of
   the last event you processed (a live frame can be ahead of the store).
3. Reconnect to your configured entry host with exponential backoff starting
   at 1000 ms; never tighter than 1000 ms after a 4008 (you were disconnected
   for not reading). After a `1001` `leader change, reconnect` the venue stays
   up: reconnect at once, re-arm cancel-on-disconnect, and expect the first
   backfill to read `REPAIRING`; a socket whose write op acks `5030` is not
   closed by the venue, so close it yourself and reconnect (rule 9 of section
   2).
4. `auth`, then `subscribe`, and START BUFFERING live frames immediately; do
   not process them yet.
5. Page `GET /v1/stream/events` from your resume point until `hasMore` is
   false and `store.status` is `COMPLETE` (or until `nextCursor` passes the
   first buffered live frame), applying rows idempotently; a page with
   `hasMore` false or `count` 0 ends the loop, never `nextCursor` equal to
   your `afterCursor`. Keep one cursor per filter.
6. Deduplicate backfill against buffered frames by `(seq, eventIndex, leg)`,
   equivalently the fixed-width cursor, PLUS the natural key where one cursor
   carries two deliveries (`tradeId`+`role` for `execution`).
7. Drain the buffer, discarding everything at or below your cursor, but only
   for the families that backfill read (a filtered cursor passes every seq up
   to the watermark), then resume live processing.
8. Re-read state-class data you use: books via `resync`, positions per
   section 13, `venueEvents` via its fresh subscribe-time `SNAPSHOT` plus
   `GET /v1/status`, and, if you trade RFQ markets, `/v1/rfq/mine`,
   `/v1/rfq/open` and `/v1/barriers` (never in the cursor backfill).

**FIX members recover the same way.** The FIX gateway keeps NO message store,
by design: sequence numbers reset on every logon (`ResetSeqNumFlag(141)=Y`
required), a ResendRequest is answered with a GapFill, and after any
disconnect you reconcile over the SAME REST backfill described above.
`ExecID(17)` is `{engineSeq}-{eventIndex}`, directly comparable with the
cursor space: buffer inbound ExecutionReports on reconnect, page
`/v1/stream/events`, dedupe by `ExecID`, then drain. Do not build a FIX
resend/persistence layer; batch entry, amendment and cancellation are also
not on FIX (loop singles, section 10).

## 15. Order lifecycle: statuses your state machine must cover

**`status`** (closed set): `NEW`, `PARTIALLY_FILLED`, `FILLED`, `CANCELED`,
`EXPIRED`, `REJECTED`, `PENDING_TRIGGER`.

- **`PENDING_TRIGGER` appears EVERYWHERE an untriggered conditional does:**
  on the place ack, on `GET /v1/orders/{id}` and `/v1/orders/open`, on WS
  `order` frames, and on stream backfill rows, including `ORDER_AMENDED`
  events while still untriggered. When the trigger fires you receive
  `OrderTriggered` and the order enters matching (then `NEW`,
  `PARTIALLY_FILLED`, `FILLED` as usual). Do not model conditionals as `NEW`.
- **`PARTIALLY_FILLED` is a state-read value.** The open-order and order
  reads carry it; on the WS `order` channel and its backfill rows every frame before
  the terminal one reads `NEW` (`PENDING_TRIGGER` before a conditional
  triggers), an `ORDER_AMENDED` of a part-filled order included, and partial
  fills arrive on `execution` only: take the filled quantity as `qty` minus
  `remainingQty`.
- **`acceptedTs` names a different instant per surface** (member API section
  6, revision 3.83 states it): on every order frame, backfill row and answer
  rendered from an order event (the `DELETE /v1/orders/{orderId}` answer, the
  rows of a cancellation batch, a `PUT` answer that ended the order, a read of
  an order already terminal) the sequencing time of that event; on the
  open-order reads the placement time, moved only by an amendment of a working
  order that changes the price or increases the quantity; on
  `GET /v1/orders/history` the first acceptance on a `FILLED`, `CANCELED` or
  `EXPIRED` row (with `closedTs` the terminal event) and the rejection time on
  a `REJECTED` row. On FIX, `TransactTime(60)` carries the event time.
- **The single amend and cancel acks carry no order.** A successful WS
  `order.amend` or `order.cancel` acks with no `data`; read the applied state
  from the `order` channel, `get.order` or the REST `PUT` and `DELETE` answers.
- **A venue reduce-only trim** reaches resting and untriggered conditional
  reduce-only orders alike, newest first, after your member position shrinks:
  it arrives unsolicited as `OrderAmended` (FIX `150=5`), or as `OrderCanceled`
  with `REDUCE_ONLY_VIOLATION`.
- **`type` and `tif` are always populated** on every event-derived order
  payload (acks, WS frames, terminal reads, backfill rows) with the
  documented spellings from section 9. You never need to infer them.
- Terminal states are `FILLED`, `CANCELED`, `EXPIRED`, `REJECTED`. Every
  terminal event carries `cancelReason` or `rejectCode`. A `duplicate: true`
  response is NOT a new lifecycle: it is a re-read of the original order.
- WS `order` channel event types: `OrderAccepted`, `OrderFilled`,
  `OrderCanceled`, `OrderExpired`, `OrderAmended`, `OrderTriggered`,
  `OrderRejected`; the backfill spells them `ORDER_ACCEPTED`,
  `ORDER_FILLED`, `ORDER_CANCELED`, `ORDER_EXPIRED`, `ORDER_AMENDED`,
  `ORDER_TRIGGERED`, `ORDER_REJECTED` in `eventType`.
- Model one extra local state, UNSOLICITED, for venue-initiated executions
  with `refSource: "VENUE"` that reference no order you submitted
  (`KX-LQ-{orderId}`, `KX-ADL-{tradeId}`); key the detection on `refSource`,
  never on the shape of the reference.

**`cancelReason`** (closed set): `USER_REQUESTED`, `IOC_REMAINDER`,
`FOK_UNFILLABLE`, `GTD_EXPIRED`, `STP_CANCEL`, `LIQUIDATION_CANCEL`,
`ADMIN_CANCEL`, `INSTRUMENT_HALT`, `MEMBER_SUSPENDED`,
`REDUCE_ONLY_VIOLATION`, `SPOT_UNFUNDED`, `POSITION_SIZE_LIMIT`, `MARKET_ACCESS`
(revision 3.65, venue-initiated: a `DENIED` access decision or an admission flip),
`FUNDING_CATCH_UP` (revision 3.70, venue-initiated: a conditional that triggered
during the market's GRID funding catch-up; reduce-only conditionals execute),
`POST_ONLY_WOULD_CROSS` (revision 3.70, venue-initiated: a post-only order that
rested through a call on a scheduled SECURITY market and would cross on the entry
into continuous trading).
`USER_REQUESTED` covers every cancel of your own: a cancel on any transport, a
member-wide cancel-all, the member portal's kill switch, and the cancel on
disconnect you armed, which reports it by design (member API section 10.5a);
the value does not tell them apart, and `ADMIN_CANCEL` is reserved for cancels
by venue operations. A cancel on disconnect is ONE command (its frames share
one `asOfSeq`), follows an armed session's close and its grace window, covers
every open order of yours and answers no request of yours.
`LIQUIDATION_CANCEL` is your signal that the venue pulled the order during a
liquidation episode. `POSITION_SIZE_LIMIT` is venue-initiated and means
settling the fill would have taken the position beyond the venue's maximum
representable size; it is not reachable at any tradable size, and like every
venue-initiated cancel you must not retry the same order unchanged.

**`refSource` on executions:** `MEMBER`, `VENUE`.

## 16. Mark-price staleness advisories (widen or pause)

Every market with a staleness policy has a per-market staleness threshold on
the age of its last mark-price tick. The ladder has three steps, and the
early-warning step is broadcast so market makers can act BEFORE the market
flips:

1. **Warning at HALF the threshold.** `venueEvents` type `STALE_MARK_WARNING`
   (both sockets), broadcast once per staleness episode. The market is still
   fully tradable. `data`:

```json
{"channel":"venueEvents","type":"STALE_MARK_WARNING","eventId":"advisory-905220-1",
 "asOfSeq":905220,"ts":1785000501000,
 "data":{"symbol":"BTC-USD-PERP","tickAgeSec":151,"reduceOnlyInSec":149}}
```

   `tickAgeSec` is the mark's age in seconds when the warning fired;
   `reduceOnlyInSec` is the time remaining before the market goes reduce-only
   (`0` when the threshold is already reached or passed).

2. **Reduce-only at the threshold.** If the feed is still silent, the market
   enters reduce-only through an ordinary `STOP_TRADING` with
   `tradingState: "REDUCE_ONLY"` and `reason: "STALE_MARK_PRICE"`. While
   reduce-only: any order that could open or grow exposure rejects with 3002;
   orders flagged `reduceOnly: true` are still accepted; cancels are
   unaffected; spot SELLS still pass (they only raise cash) while spot buys
   reject 3002; RFQ creation rejects 3002 (an RFQ opens new risk).

3. **Automatic recovery.** Fresh ticks lift reduce-only via `RESUME_TRADING`
   with `reason: "MARK_PRICE_RECOVERED"`, and `STALE_MARK_RECOVERED` closes
   the episode once the age is back under half the threshold
   (`data`: `{"symbol": ..., "tickAgeSec": ...}`). If the feed recovers before
   the market ever flipped, `STALE_MARK_RECOVERED` alone closes the episode.

**Broker reaction:** treat `STALE_MARK_WARNING` as the signal to WIDEN QUOTES
or PAUSE QUOTING on that market (the mark that risk, triggers and the price
band run on has stopped moving), use `reduceOnlyInSec` as your countdown to
switch flow to close-only, and treat `STALE_MARK_RECOVERED` as the all-clear.

**Advisory semantics (do not over-trust):** the advisory types are
best-effort and informational. On the private socket the advisory
`MARGIN_WATCH` (member reference revision 3.70) tells you that your margin
utilisation crossed the venue's early-warning level (`state` `ABOVE`) or fell
back below it minus 5 points (`CLEARED`), `data` `{state, ccy, utilisationPct,
thresholdPct, reArmPct}`: top up collateral or reduce before the engine's
`MARGIN_CALL`, and never drive your risk state off it (it is not a margin call). `eventId` namespace `advisory-{asOfSeq}-{n}`
(a valid dedupe key across your two sockets); never in the `SNAPSHOT`, never
in `/v1/stream/events`, never backfilled or replayed; a venue restart
mid-episode may repeat a warning once (treat a repeat as a reminder, not a new
episode). NEVER drive your trading-state machine off advisories:
state transitions arrive exclusively as `STOP_TRADING`, `RESUME_TRADING` and
`SNAPSHOT`. A client that connects mid-episode learns of the situation only
after the market actually flips.

## 17. Enumerations your parsers must cover (closed sets as of member doc revision 3.4)

Order `status`, `cancelReason` and `refSource` are in section 15.

**venueEvents `type`:** `SNAPSHOT`, `STOP_TRADING`, `RESUME_TRADING`,
`LIQUIDATION`, `ADL`, `MARGIN_CALL`, `LIQUIDATION_COMPLETE`, plus the
advisory types `STALE_MARK_WARNING` and `STALE_MARK_RECOVERED` (section 16)
and, on the private socket only, `MEMBER_PARAMS` (revision 3.61) and the
advisory `MARGIN_WATCH` (revision 3.70).
Treat the set as open (ignore unknown types).

**`reason` on `STOP_TRADING`:** `ADMINISTRATIVE`, `CIRCUIT_BREAKER`,
`STALE_MARK_PRICE`, `DELISTING`. **On `RESUME_TRADING`:** `ADMINISTRATIVE`,
`CIRCUIT_BREAKER_EXPIRY`, `MARK_PRICE_RECOVERED`. `tradingState` values:
`TRADING`, `HALTED`, `REDUCE_ONLY`, `POST_ONLY`, `DELISTED`. Treat an
unrecognised stop reason as a stop you may not clear on your own.

**Error codes** (`code` in the envelope, `rejectCode` on rejected orders):

| Code | Meaning | Code | Meaning |
|---|---|---|---|
| 0 | OK | 2019 | Malformed backfill cursor (incl a field above its range, section 14) or unknown channel family |
| 1001 | Auth failed | 2020 | Reserved KX- clOrdId namespace |
| 1002 | Timestamp outside window | 2021 | reg.traderId missing or not a registered Authorised User |
| 1003 | Rate limit (carries retryAfterMs, section 12) | 2022 | RFQ market: no order book, use /v1/rfq |
| 1004 | Order-to-trade regime block | 2023 | Not an RFQ market: use the order book |
| 1005 | READ_ONLY credential refused on a write | 2024 | RFQ not found, closed or expired (HTTP 404) |
| 2001 | Unknown symbol | 2025 | Quote not found, withdrawn or expired (HTTP 404) |
| 2002 | Tick/qtyStep misalignment | 2026 | RFQ self-quote / self-accept |
| 2003 | Price outside band (incl a stale RFQ quote at accept) | 2027 | Barrier terms invalid for the market and side |
| 2004 | Unsupported order type (incl any trailing-stop) | 2028 | Order/RFQ notional below minNotional (section 3) |
| 2029 | Venue end-client identifier capacity reached: a clientUid the venue has never seen (known ones unaffected) | | |
| 2005 | Post-only would cross | 3001 | Insufficient margin / spot quote funds |
| 2006 | FOK unfillable | 3002 | Member status forbids action (incl reduce-only sessions) |
| 2007 | No eligible liquidity | 3003 | Instrument halted |
| 2008 | Reduce-only violation | 3004 | Insufficient base asset (spot sell) |
| 2009 | Duplicate clOrdId, different payload | 3005 | Instrument has no mark price yet |
| 2010 | clientUid required | 3006 | Instrument delisted (permanent for that listing; a re-listing can bring the symbol back, section 3) |
| 2011 | Position cap: worst case at the current mark where `maxPositionNotional` is set, else `maxPositionQty`; gates increases only. Reduce or wait for capacity, never retry blindly (sections 3 and 5) | 3007 | RFQ accept: quoting member fails margin (quote stays listed) |
| 2012 | Too many open orders/RFQs/quotes | 4001 | Market-segments version conflict |
| 2013 | Malformed field (since Member API 3.72 also a `clOrdId`, a `clientUid` or a Trader ID of the `reg` block that begins or ends with whitespace: trim it, never retry as sent), or order-size cap: `maxOrderNotional` at the order's effective price (limit price if priced, current mark if market-type) where set, else base-unit `maxOrderQty`; `reduceOnly` NOT exempt. Split the order, fresh `clOrdId` per piece; applies per batch entry too (sections 3, 5, 10) | 5000 | Internal error (order entry: reconcile, section 5) |
| 2014 | Order not found | 5001 | Engine busy (retry with backoff; a single write answered 5001 during a leader change may still execute, section 5) |
| 2015 | Self-trade prevented (REJECT_TAKER) | 5002 | Venue halted |
| 2016 | Unsupported on spot | 5003 | Invalid request parameter (msg names it) |
| 2017 | Unknown field in request body | 5004 | Event store not configured (deployment) |
| 2018 | leverage/quoteCcy assertion mismatch | 5005 | Command accepted and recorded, then not completed: reconcile what it did achieve, never resubmit unchanged |
|  |  | 5030 | Not the cluster leader: a redirect, never a rejection; resubmit the same clOrdId on your configured entry host, which `leader` must equal (rule 9 of section 2) |

Distinguish 3003 (halted: wait for RESUME_TRADING) from 3005 (no mark yet: wait
for the feed) from 3006 (delisted: stop trading that listing).

## 18. WebSocket delivery contract (summary)

- Every request on either socket is one JSON frame `{"op": ..., "reqId": ...,
  "args": [...]}` answered by exactly one ack `{"op", "reqId", "success",
  "code", "msg", "data"}`. Subscriptions pass channel-name strings in `args`,
  identically on both sockets, for example after `auth` on `/ws/private`:
  `{"op":"subscribe","reqId":"2","args":["order","execution","wallet","position","venueEvents"]}`.
  `unsubscribe` mirrors it, and an unknown channel name acks
  `success: false` with code 2013.
- Guaranteed channels (strictly ordered, never silently dropped): `order`,
  `execution`, `wallet`, `eligibility`, `adl` executions, `unwind`, `margin`
  marginCall, `venueEvents`, `rfq`, `barriers`, and every request/reply. If
  the server cannot keep the guarantee for your connection it sends a
  terminal `gap` frame (naming which families are backfillable and from which
  cursor) and closes with 4008. Do not reconnect faster than 1000 ms after a
  4008. `rfq` and `barriers` are guaranteed but NOT cursor-backfillable:
  after a gap, re-read `/v1/rfq/mine`, `/v1/rfq/open` and `/v1/barriers`
  instead (state-only recovery, section 11).
- Best-effort channels (may conflate under pressure, announced by a
  non-terminal `CONFLATED` gap frame with the dropped range): `orderbook`,
  `matchableBook`, `trades`, `ticker`, `markIndex`, `funding`, `status`,
  `position`, `clientPosition`, `margin` updates, `adl` indicators. Resync
  books with `{"op":"resync","args":["orderbook.25.BTC-USD-PERP"]}` (name the
  book channels you want re-snapshotted) or the REST snapshot; recover the
  rest with state reads (positions per section 13).
- Idle sockets close with 4009 after 60 s of inbound silence; send
  `{"op":"ping"}` at least every 30 s. The server also sends heartbeats on
  silent sockets; a heartbeat is not data.
- Private frames carry `asOfSeq` and `eventIndex`: the engine's total order.
  Use them (with the leg) for dedupe against backfill (section 14). Within a
  channel, frames arrive in ascending (`asOfSeq`, `eventIndex`) order;
  ordering ACROSS channels is not guaranteed, so correlate by ids, not
  arrival.
- Order entry ops: `order.place`, `orders.batch`, `orders.batchAmend` and
  `orders.batchCancel` (section 10), `order.cancel`, `order.cancelAll`,
  `order.amend`. `order.cancelAll` reads `symbol` and `side` (`side` exactly
  as `DELETE /v1/orders?side=`) and cancels EVERYTHING without them; since
  member API 3.84 any other member is refused 2017 (a batch body sent to it by
  mistake among them), another `side` 5003, a `null` or non-string value,
  `args` given twice and args that are not exactly one object 2013, each with nothing cancelled
  (before, every other member, `side` included, and a `null` `symbol` were
  ignored and every open order the `symbol` matched was cancelled, without one
  every order). Pipelining is supported
  and recommended: send without waiting for each ack; the venue processes in
  arrival order. A successful `order.amend` or `order.cancel` acks with no
  `data` (section 15). On a node that is not the leader every write op acks
  `5030` with `leaderId` and `leader` under `data` and the socket stays open:
  close it yourself and reconnect (rule 9 of section 2); on a clustered
  deployment a write whose commit the venue did not confirm within 10 seconds
  acks `5000` (since member API revision 3.83), outcome unknown (section 5).
- Close `1001` carries one of two reasons: `venue restart`, or, on a clustered
  deployment, `leader change, reconnect` after the advisory `leaderChange`
  (the venue stays up; reconnect, re-arm cancel-on-disconnect, recover).
- Synchronous reads mirror REST over the socket: `get.account`,
  `get.positions`, `get.clientPositions`, `get.openOrders`, `get.executions`,
  `get.streamCursor` and more, same payload shapes as REST. The dated history
  routes, the EOD reconciliation read and `/v1/stream/events` are REST-only.
- The WS `wallet` field `balance` equals the REST ledger's `balanceAfter`
  (both spellings are carried); the WS `margin` channel's lowercase `mm` is
  `GET /v1/account`'s uppercase `MM`. Same numbers, frozen legacy spellings.

## 19. FIX market data (decision guide)

FIX 4.4 on this venue is order entry, drop copy AND market data: an aggregated
book per instrument plus a public trade tape. Skip this section if you integrate
over the native pair, where books, trades, ticker, mark, index, funding and
venue events are WebSocket channels (sections 7 and 18). What follows is every
DECISION a FIX market-data consumer has to make and the venue behavior each one
buys. The tag dictionary, message bodies field by field with every reject code,
is `KronosX-Claude-Code-FIX.md` section 9; this pack deliberately does not
duplicate it.

**Subscribing.** One `MarketDataRequest(35=V)` per symbol (`NoRelatedSym(146)`
must be 1) carrying your own `MDReqID(262)`, which the venue echoes on every
reply and every reject. `SubscriptionRequestType(263)`: `0` snapshot once, `1`
snapshot then updates, `2` unsubscribe. Unsubscribe is silent (no reply
confirms it) and a message already in flight may still land, so treat the first
quiet period as the boundary. An optional `NoMDEntryTypes(267)` group of
`MDEntryType(269)` values narrows the feed to `0` bid, `1` offer, `2` trade;
absent means all three. `262`, `263` and `55` are REQUIRED and are rejected by
name when absent: the venue does not substitute a value for a field that decides
what the request means, so a request that forgets `263` is refused rather than
served as a single snapshot. That includes the `2` unsubscribe, which is the one
place a refusal leaves something running: the venue drops the subscription by
symbol, so a `263=2` that omits `262` is refused and your feed KEEPS DELIVERING.
The rule has a third clause on the other side: the venue also never REPLACES a
value you actually sent, so a `MarketDepth(264)` it cannot serve is refused with
`281=5` rather than adjusted to one it can (see Decision 2 below). A `263=2`
disable is read from `263` and `55` alone, so `264` on a disable is ignored
rather than validated. BODY field order is not significant anywhere outside a
repeating group; the standard header and trailer stay positional as FIX 4.4
requires, and a tag sent twice keeps its first occurrence.

**Decision 1: full refresh or incremental.** `MDUpdateType(265)` on a `263=1`
subscription. This is the only real choice in the feed, and changing your mind
means re-subscribing. On a `263=0` snapshot-once request there is nothing to
increment, so `265` shapes nothing and the reply is the same single snapshot
either way.

| | `265=0` or absent: full refresh | `265=1`: incremental |
|---|---|---|
| What arrives | a complete fresh `MarketDataSnapshotFullRefresh(35=W)` after every commit batch that changed the book or the last trade | one `35=W` to seed, then one `MarketDataIncrementalRefresh(35=X)` per commit batch carrying only what changed plus that batch's prints |
| Your state | none: apply the newest message, discard the previous one | a price-keyed book per symbol that you maintain |
| Under pressure | conflates for free, because the next refresh is complete and current anyway | cannot silently skip an update, so the venue re-baselines you with a fresh `35=W` |
| Bandwidth on a busy or deep book | the whole window, every batch | the changed levels only |
| Trade information | last trade PRICE only, no size, no id | the batch's prints with venue trade id, price, size, time, aggressor side and trade classification |
| Default | YES, and unchanged for sessions that never ask | opt in |

`265=0` is the default deliberately: a client written before this feature keeps
receiving exactly what it always received. Choose `265=1` when you want the
trade tape or your book is deep enough that resending the whole window every
batch costs you. Choose `265=0` when you want a stateless consumer that cannot
drift.

Both shapes are event driven, at most one message per subscribed instrument per
commit batch, never a timer: a quiet book produces no traffic. If your session
was behind and a refresh was dropped, the venue still sends you one once you
have drained, even if the book has gone quiet in the meantime, so you never sit
on a stale view waiting for the next trade.

**Decision 2: depth.** `MarketDepth(264)` is levels per side: 5 if you omit it,
and if you SEND it, an integer from 1 to 25. 25 is the maximum SERVABLE depth,
and anything else, including `0` (full book in FIX 4.4), a larger value or a
non-numeric string, is REFUSED with `MDReqRejReason(281)=5` and subscribes
nothing, rather than being adjusted to a depth the venue can serve. Ask
for what you will actually use: the depth you choose is the window the venue
diffs against on your behalf, and a level rising into that window from below
arrives as a new level, correctly, rather than as a silent omission.

**Loss detection and recovery. These four rules are the contract:**

1. **`MsgSeqNum(34)` is the loss detector.** Market data shares the session
   sequence with your ExecutionReports, so a gap is visible in exactly the same
   way, with no separate market-data sequence to track.
2. **Any `35=W` arriving mid-stream is a RE-BASELINE, never a delta.** The venue
   sends one when your session fell behind and it stopped sending increments
   rather than racing your queue. Discard your book for that symbol and rebuild
   it from that message. A consumer that merges a `W` into an existing book
   silently doubles levels and never recovers.
3. **Recovery is to re-subscribe.** There is NO market-data resend and no
   message store: re-issuing the `MarketDataRequest` at any time returns a fresh
   `35=W` and re-seeds the venue's baseline, and that is the entire procedure.
   The replacement takes effect immediately, so anything the venue had already
   built for the subscription you replaced is discarded rather than delivered
   after the new `W`. You can therefore narrow the depth or the entry-type set
   on a re-issue without inheriting entries from the old window. After a
   disconnect, reconnect (sequence numbers reset on logon) and re-subscribe.
4. **Key the book by price.** Incremental book entries carry the level's rank at
   the time of that message; a new or deleted level shifts every level below it
   without the venue restating them. That rank is for display and sanity checks,
   never an identifier. Sizes on a change are absolute, so re-applying a change
   is a no-op and a delete carries size zero.

**Limits, and what happens at them.**

- 25 price levels per side, per subscription. That is the maximum SERVABLE
  depth: a request above it is refused with `281=5`, never trimmed to fit.
- **32 instrument subscriptions per session.** Over the cap the request is
  rejected and subscribes nothing: unsubscribe one first. Re-subscribing a
  symbol you already hold REPLACES that subscription and does not consume a
  second slot, and a `263=0` snapshot-once request registers nothing and does
  not count at all. A FIX session here is an order-entry connection with a price
  reference attached, not a market-data distribution point. If you need many
  symbols, take the book over WebSocket.
- **Trade prints are BEST EFFORT.** A session that falls behind loses prints,
  and the re-baseline `35=W` restores the book but not the prints you missed.
  `GET /v1/trades` is the gap-free tape INSIDE THE WINDOW IT COVERS, which is
  the most recent 1000 prints of that instrument: it is a recent-trades route
  and not an archive, so poll it well inside that window rather than treating it
  as a backstop you can reach for hours later. The venue trade id on a FIX print
  is the same id REST and the WebSocket `trades` channel carry, so prints dedupe
  across transports: use FIX for immediacy and REST for completeness.
- Anything the venue will not serve answers `MarketDataRequestReject(35=Y)`
  with a reason code (missing or unknown symbol, or bad request shape; at the
  subscription limit, the engine momentarily busy, or a snapshot the venue could
  not build, all three retryable; missing or unsupported subscription type;
  unsupported depth, meaning a `264` present and outside 1 to 25; unsupported
  update type, unsupported entry type), and `Text(58)` names the tag
  at fault. The one exception is a missing `MDReqID(262)`, which answers a
  session-level `Reject(35=3)` because a `35=Y` must itself carry the `MDReqID`.
  A rejected request subscribes NOTHING: never assume a partial subscription.

**Not carried on FIX**, with where to get it: per-level book checksums, mark and
index prices, funding rates and the typed venue-events feed are WebSocket only
(section 18); dated history and the gap-free recent-trades tape, the most recent
1000 prints per instrument, are REST (section 8).

**The WebSocket alternative, so your choice is informed.** The venue publishes
incremental books over WebSocket too, on `/ws/public` as `orderbook.25.{sym}`
(snapshot then deltas), with a CRC32C `checksum` on every update: a consumer can
PROVE its book matches the venue's on every message instead of inferring it.
That channel also offers `{"op":"resync","args":["orderbook.25.BTC-USD-PERP"]}`
to re-snapshot one book without disturbing the socket, and a non-terminal
`CONFLATED` gap frame naming the range you lost. FIX market data has neither a
book checksum nor a per-channel resync; its equivalents are the re-baseline
`35=W` and the re-subscribe, both of which are whole-subscription operations. If
book integrity or symbol count matters more to you than staying on one socket,
take market data over WebSocket and leave order entry on FIX.

## 20. Venue-initiated flow (what your handlers must absorb)

1. A margin breach opens a liquidation episode: EVERY working order of yours is
   canceled venue-wide (each cancel names its `clOrdId`, `clientUid`,
   reason `LIQUIDATION_CANCEL`), then the venue works closing orders per end
   client (`KX-LQ-{orderId}`), and `venueEvents` streams `MARGIN_CALL`,
   `LIQUIDATION` and finally `LIQUIDATION_COMPLETE` (which also arrives when an
   episode cures without any fill). The `LIQUIDATION` event's `positionRef`
   (`clientUid + "|" + symbol`) resolves each venue close to exactly one
   end-client sub-position in your books.
2. ADL: if the book cannot absorb a close at the bankruptcy price, opposite
   positions are torn up pro-rata across the affected member's end clients at
   that price; you see per-client executions flagged `ADL` with
   `KX-ADL-{tradeId}`, and `venueEvents` type `ADL`. Your client allocation must
   accept closes that the affected client did not cause.
3. Trading state: `STOP_TRADING` carries the instrument scope and a typed
   `reason` (`ADMINISTRATIVE`, `CIRCUIT_BREAKER`, `STALE_MARK_PRICE`,
   `DELISTING`); `RESUME_TRADING` follows on recovery (`ADMINISTRATIVE`,
   `CIRCUIT_BREAKER_EXPIRY`, `MARK_PRICE_RECOVERED`). While halted, orders
   reject with 3003; while reduce-only, opening orders reject with 3002
   (section 16). A `DELISTING` stop cancels every resting order of the market
   with `INSTRUMENT_HALT`; it is how a re-listing begins (section 3).
4. RFQ markets: a member entering liquidation has its barrier contracts torn
   up at the barrier-clamped mark and its open RFQs and quotes withdrawn, and
   a perp position on an RFQ market closes AT MARK against opposite-side
   holders as an ADL-flagged trade (section 11). Absorb these like any other
   venue-initiated close.

## 21. The DO NOT list

- Do NOT send trailing stops in any form: the type does not exist here and any
  trailing parameter rejects with 2004 (icebergs do not exist either).
- Do NOT mutate a signed request after signing: no re-serialization, no URL
  re-encoding, no header-driven rewrites. Sign the exact bytes on the wire
  (section 4).
- Do NOT derive positions from the `position` or `clientPosition` channels.
  They are best-effort hints; position arithmetic comes from `execution` only,
  anchored by the seeding recipe (section 13).
- Do NOT resubmit an order under a NEW `clOrdId` while the previous attempt's
  outcome is indeterminate (timeout, 5000, a single write's 5001 during a
  leader change). Resubmit the SAME `clOrdId` or reconcile first (section 5).
- Do NOT treat a 5030 as a rejection, do NOT send credentials to a `leader`
  that is not your configured entry host, and do NOT pin a node address
  (rule 9 of section 2).
- Do NOT carry a cached `tickSize` or `qtyStep` across an announced
  re-listing; key instruments on the symbol (section 3).
- Do NOT blind-resend `POST /v1/rfq` after a timeout: RFQ creation has no
  idempotency key; reconcile with `/v1/rfq/mine` and cancel the duplicate.
- Do NOT try to cursor-backfill `rfq`, `barriers` or `venueEvents` (2019):
  recovery for those is state re-read or the fresh `SNAPSHOT` (section 14).
- Do NOT hard-code instrument parameters, the instrument list, or rate-limit
  numbers; read them from the venue and re-read after any announced reset.
- Do NOT hard-code resting-order price offsets: the price band is checked
  against the CURRENT mark at placement, so derive offsets from `priceBandBps`
  and a live mark (`GET /v1/ticker` or the `markIndex` channel). A fixed
  "safe" price drifts out of the band as the mark moves and draws
  intermittent 2003s that look random.
- Do NOT mint a fresh `clientUid` per session or per order: identifiers are
  venue-wide, capped at 1,048,576 and never pruned (each carries an end
  client's lifetime realized PnL and funding), so a generator that leaks
  fresh values consumes shared capacity permanently and ends in `2029`.
  `clientUid` is the ultimate client's stable reference; `clOrdId` is the
  per-message one.
- Do NOT assume unit caps: read `maxPositionNotional` and `maxOrderNotional`
  from `GET /v1/instruments` and size hedges against the live mark. Where a
  market publishes them, the base-unit `maxPositionQty`/`maxOrderQty` fields
  (and any per-member unit override) are informational legacy and not
  enforced (section 3).
- Do NOT use floats for prices, quantities or money, anywhere.
- Do NOT send fields the contract does not define (2017), and do NOT build
  strict parsers of venue RESPONSES (the venue adds fields and types without
  notice).
- Do NOT prefix your `clOrdId` with `KX-` (2020), and do NOT key
  venue-initiated-close handling on the shape of a reference; key it on
  `refSource: "VENUE"`.
- Do NOT process live frames before the reconnect backfill completes; buffer
  first (section 14).
- Do NOT treat envelope `code: 0` as "order accepted": check `data.status`
  and `data.rejectCode` (rule 8).
- Do NOT merge a mid-stream FIX `35=W` into an existing book. On an incremental
  market-data subscription every later snapshot is a re-baseline: discard that
  symbol's book and rebuild from it (section 19).
- Do NOT key a FIX market-data book on the entry's level rank, and do NOT treat
  the FIX trade tape as gap-free: key the book on price and reconcile prints
  against `GET /v1/trades` (section 19).
- Do NOT send a FIX `MarketDepth(264)` you have not validated against the
  published range and expect the venue to trim it: an unservable depth is
  refused and subscribes nothing (section 19).
- Do NOT wait for an ExecutionReport after a FIX `35=D` that omitted
  `Symbol(55)`, `Side(54)`, `OrderQty(38)` or `OrdType(40)`, or that carried a
  `Side(54)` outside the FIX 4.4 enumeration. The answer is a session-level
  `Reject(35=3)` naming the tag, and it carries no `ClOrdID(11)`. Those refusals
  also spend no order-budget token and are not counted in your order-to-trade
  ratio: the venue never accepted the message for processing.
- Do NOT send a FIX tag with an EMPTY value where you mean "not sent" and expect
  a different answer: the venue reads present-but-empty as missing everywhere,
  including `SenderCompID(49)` on Logon (you are then addressed as `CLIENT`) and
  `OrderID(37)`, `ClOrdID(11)` and `OrigClOrdID(41)` on a cancel or replace
  (the reject carries `37=0`, `11=KX-NONE`, `41=KX-NONE`, all present).

## 22. Integration test checklist (run against sandbox before go-live)

1. Signer reproduces BOTH section 4 worked examples, including the raw-comma
   query vector.
2. Place LIMIT GTC far from market; assert `NEW`, then cancel; assert
   `CANCELED`/`USER_REQUESTED` with your `clOrdId` and `clientUid` echoed.
3. Send an order with an extra made-up field; assert code 2017 (proves you are
   on the strict contract).
4. Send `leverage: 3` (or any wrong value); assert 2018.
5. Send a `qty` off the `qtyStep` grid; assert 2002. Send a valid step whose
   notional is under 5 USD; assert 2028. Then close a small position with an
   exact-size `reduceOnly` order below the floor; assert it is ACCEPTED (the
   reduce-only exemption works).
6. MARKET+FOK larger than visible depth; assert single cancel `FOK_UNFILLABLE`,
   zero partials.
7. Duplicate `clOrdId` retry: identical payload returns `duplicate: true` and
   the SAME `orderId`; changed payload returns envelope 2009 with
   `data.clOrdId` echoed.
8. Place an untriggered STOP_MARKET; assert status `PENDING_TRIGGER` on the
   ack, on `GET /v1/orders/open`, and that `type`/`tif` are populated on every
   frame.
9. Batch: send a 3-entry batch where entry 2 is deliberately off-grid; assert
   HTTP 200, `data.results` length 3 in array order, entries 1 and 3
   succeeding, entry 2 = `{code: 2002, msg, clOrdId}`.
10. Drive a 1003 (tight loop on a scratch key); assert you sleep exactly
    `retryAfterMs` and the resend with the same `clOrdId` succeeds.
11. Maintain `orderbook.25` for 5 minutes with checksum verification.
12. Kill and recover the private socket; prove gap-free merged event log via
    `/v1/stream/events` cursors (section 14).
13. Seed baselines per section 13, trade a simulated day, and reconcile your
    per-client table against `/v1/reconciliation/end-client-positions`;
    assert an exact match.
14. Force a small liquidation on a scratch sub-account (venue ops can assist)
    and verify your unsolicited-close handling end to end, including
    `positionRef` resolution.
15. If you trade RFQ markets: create an RFQ on `BTC-USD-KO`, quote it from a
    second sandbox credential (venue ops can provide one), accept, then verify
    the `rfq` `accepted` frames on both sides, the `barriers` `opened` frame,
    and the contract on `GET /v1/barriers`.
16. If you take market data over FIX (section 19): subscribe incremental on one
    symbol, maintain the book, and at a quiet moment assert it equals
    `GET /v1/orderbook?symbol=&depth=` at the same depth. Then re-issue the same
    request with a smaller depth and assert you get a fresh snapshot, that your
    consumer REBUILDS from it rather than merging, and that no entry from the
    old, wider window survives. Then the negative case: send `264=0` and
    `264=50` and assert `281=5` with nothing subscribed and your previous
    subscription state untouched. Finally, assert your tape consumer dedupes its
    prints against `GET /v1/trades` by venue trade id.
17. If you trade over FIX: omit `Symbol(55)`, then `Side(54)`, then
    `OrderQty(38)`, then `OrdType(40)` from a `35=D`, one at a time, and assert
    `Reject(35=3)` with `373=1` and the matching `RefTagID(371)`, that no
    ExecutionReport follows, and that your outstanding-request counter clears
    without waiting for one. Assert your parser accepts an ExecutionReport with
    no `ClOrdID(11)` and an `OrderCancelReject(35=9)` carrying `11=KX-NONE`.
    Then send `54=X` and assert `Reject(35=3)` with `373=5` and `371=54`, and
    `54=5` and assert the unchanged `150=8` with `9010=2013`.
18. If you trade over FIX: send a `35=F` with `37=`, `41=` and `11=` all EMPTY
    and assert an `OrderCancelReject(35=9)` carrying `37=0`, `11=KX-NONE` and
    `41=KX-NONE`, all three PRESENT, that your session layer accepts. Log on with
    `SenderCompID(49)` empty, and separately send anything before your Logon, and
    assert both answers carry `TargetCompID(56)=CLIENT` and never an empty `56`.
19. Leader change (Conformance Test Plan I-05), at the leader hand-over venue
    operations stage on the clustered sandbox at an agreed time: assert your
    connector follows a `5030` only to your configured entry host, sends the
    request again there with the SAME `clOrdId` (a cancel at once), never counts
    it as a reject, closes and reopens a WebSocket that acked `5030`, handles
    the `1001` `leader change, reconnect` close and the FIX leader-change
    Logout, re-arms cancel-on-disconnect, recovers through a `REPAIRING` store,
    and reads back by `clOrdId` a write answered `5001` or `5000` meanwhile.

## 23. Support

Interactive reference: `<base>/docs`. Machine-readable spec:
`<base>/openapi.json`. A Postman collection with auto-signing and a runnable
reference implementation (trading UI plus signing proxy) are available from
venue operations, as are sandbox credentials. Sandbox balances are test money
and resets are announced in advance.
