# Kronos X REST Integration Manual for Claude Code

Drop this file into the root of your integration repository and point Claude
Code at it. It is self-contained: everything needed to generate a correct REST
client for the Kronos X MTF, in any language.

Suggested first prompt:

> Read KronosX-Claude-Code-REST.md and generate a Kronos X REST client in
> <language>: request signer, typed endpoint wrappers, an order state machine,
> and retry/idempotency handling. Then write a smoke test against the sandbox
> using credentials from environment variables.

Companion manual: `KronosX-Claude-Code-WebSocket.md` (streaming and recovery).
Most members need both: REST for requests and history, WebSocket for the live
event stream. Authoritative machine-readable spec: `<base>/openapi.json`;
interactive tester with in-browser signing: `<base>/docs`.

---

## 1. Environments

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

The sandbox host is `sandbox.mtf.perpetuals.net` since member API revision
3.83: 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 (section 8, `5030`).

TLS only, publicly trusted certificate, no pinning required. Credentials are an
API key and secret issued by venue operations. Read them from environment
variables (`KRONOSX_API_KEY`, `KRONOSX_API_SECRET`) or a secret store. Never
commit them.

Credentials carry a **scope**. A `TRADE` key can do everything; a `READ_ONLY`
(drop copy) key is refused on every write with code 1005. Use a READ_ONLY key
for back-office, risk and reporting components.

## 2. Non-negotiable rules

1. **Exact decimal strings.** Every price, quantity, rate and money amount is a
   STRING (`"65000.5"`). Parse into a decimal type. Never use binary floating
   point for money.
2. **Strict request bodies.** The venue REJECTS unknown JSON fields on order
   entry with code 2017 instead of ignoring them. Send only defined fields.
3. **Tolerant response parsing.** The venue ADDS response fields and enum values
   without notice. Ignore what you do not recognise; never fail on an unknown
   field or unknown enum value.
4. **Two-level success check.** `code: 0` means the gateway accepted the
   request. An order can still be rejected by the engine INSIDE a 200 response:
   check `data.status == "REJECTED"` and read `data.rejectCode`.
5. **Align to the instrument grid.** Fetch `GET /v1/instruments` at startup and
   align every price to `tickSize` and every quantity to `qtyStep` (else 2002).
   Quantities are DECIMAL BASE-ASSET UNITS (`"0.0014"` on BTC-USD-PERP is
   0.0014 BTC; integer strings remain valid), prices are per base unit, and an
   order whose `price x qty` is below the instrument's `minNotional` rejects
   with 2028. Never hard-code instrument parameters. The grid of a listed
   market (`tickSize`, `qtyStep`) never changes in place: a grid change is a
   RE-LISTING announced in writing in advance (member API section 2a). On the
   date the market is delisted (`venueEvents` `STOP_TRADING` with reason
   `DELISTING`, every working and pending-trigger order cancelled with
   `cancelReason` `INSTRUMENT_HALT`, `3006` from then on, reduce-only orders
   included), open positions are final-settled, and the market is listed again
   with the new values and an empty book under the symbol and `isin` the
   announcement names (`2001` for that symbol between the removal and the new
   listing). Nothing on the wire announces it before the date and no member
   surface serves an instrument id, so 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 across the
   date. `minNotional` and the other parameters can change in place.
6. **Route by market mode.** Every instrument carries `mode`: `ORDER_BOOK` or
   `RFQ`. An RFQ market has NO order book: `POST /v1/orders` there rejects
   2022, and trading runs through the `/v1/rfq` routes (section 7). An RFQ
   operation on an order-book market rejects 2023. Read the mode from
   reference data and route accordingly.

## 3. Authentication

Every private request carries three headers:

```
X-API-KEY:    <api key>
X-TIMESTAMP:  <unix milliseconds, as a 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:
  `/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, reuse the string).
- Optional `X-RECV-WINDOW` (ms, default 5000): an integer from 1 to 60000, any
  other value is refused 1002 (Member API 3.85; 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: code 1002.
- **A signed write is accepted ONCE** (Member API 3.85): the same `X-API-KEY` and
  `X-SIGNATURE` on any method but GET answers 1002 `request already received`
  and nothing runs. **Sign every send, a retry included, with a fresh
  `X-TIMESTAMP`** (serialize the body once, sign per attempt); keep the same
  `clOrdId` on the retry. A venue node also refuses 1002 a write stamped before it
  started or became the leader: sign it again. Never let an HTTP library resend a
  signed request on its own.

**Verify your signer against this vector before anything else:**

```
secret     = "test-secret"
timestamp  = "1700000000000"
method     = "GET"
path       = "/v1/account"
body       = ""
payload    = "1700000000000GET/v1/account"
signature  = 741b61b6d3740ede7864551612095d2fb73b75a4f70534f45e9e207bdd1f321c
```

Second vector, a comma-bearing query string transmitted RAW (not `%2C`). The
venue verifies against the query exactly as it arrives on the wire, and some
HTTP stacks re-normalize percent-encoding AFTER you build the URL, so the
signed string and the transmitted string silently diverge and every such
request fails 1001. Known case: Python `aiohttp`/`yarl` re-encodes commas;
use `yarl.URL(url, encoded=True)` or sign the exact final string on the wire.
Whatever the library, verify once in its request tracing that the transmitted
query is byte-identical to what you signed.

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

## 4. Response envelope

```json
{ "code": 0, "msg": "ok", "data": { }, "asOfSeq": 123456789, "ts": 1700000000000 }
```

`asOfSeq` is the engine sequence the response reflects: a total order across the
whole venue, comparable with WebSocket frames and the recovery cursor.

## 5. Endpoints

### Public (no signature)

| Endpoint | Purpose |
|---|---|
| `GET /v1/time` | Server time |
| `GET /v1/status` | Venue and per-instrument trading state with reasons |
| `GET /v1/instruments` | Reference data: tick/lot, fees, risk tiers, ISIN/CFI, order and position caps (`maxOrderNotional`/`maxPositionNotional` where published, legacy `maxOrderQty`/`maxPositionQty`), `maxLeverage`, `leverageMode`, `kind` (`PERP` \| `SPOT` \| `BARRIER` \| `SWAP`; SWAP is always RFQ, offers taken not quoted, Member API section 6h), `mode` (`ORDER_BOOK` \| `RFQ`) |
| `GET /v1/orderbook?symbol=&depth=` | Book snapshot with CRC32C `checksum` |
| `GET /v1/trades?symbol=&limit=` | Recent public trades |
| `GET /v1/ticker?symbol=` | Best bid/ask, mark, index, funding, open interest |
| `GET /v1/index-price?symbol=` | Index price |
| `GET /v1/mark-price?symbol=` | Mark price (what risk uses) |
| `GET /v1/mark-klines?symbol=&interval=&from=&limit=` | Mark-price candles `{t, open, high, low, close, ticks}`, the klines interval set and bound, no volume (Member API 3.70); empty on a market without a mark |
| `GET /v1/funding/current?symbol=` | Current and predicted rate, next funding time |
| `GET /v1/funding/history?symbol=&from=&to=&limit=&includeSkips=` | Settled rates; `includeSkips=true` adds the LOCAL_DAILY days that closed without a settlement as rows with `skipped` true and `skipReason` (Member API 3.70) |
| `GET /v1/insurance-fund` | Fund state |

### Private (signed)

| Endpoint | Purpose |
|---|---|
| `POST /v1/orders` | Place an order (body in section 6) |
| `POST /v1/orders/batch` | `{"orders":[...]}`: 1 to 50 complete, independent place-order bodies, each with its own `clOrdId` and `clientUid` (mass-close use case). Non-atomic; entries execute in array order and pass the identical checks as singles (order-size and position caps included: 2013/2011 ride per-entry results as `rejectCode`); `data.results` in array order, a success entry shaped like the single-order response data (engine rejects ride `rejectCode`), a gateway failure entry shaped `{code,msg,clOrdId}`; envelope 0 / HTTP 200 even all-reject. Consumes one order-budget unit per entry, up front, all or nothing (1003 consumes nothing); every entry counts toward the OTR regime. Entry member names are case-sensitive; since Member API 3.74 an entry whose outcome the venue could not confirm is a `5000` row (reconcile by its `clOrdId`, resend only with the SAME `clOrdId`) and after an entry the intake refused as it was submitted every later entry is a `5001` row (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). WS twin `orders.batch`; not offered over FIX |
| `POST /v1/orders/batch/amend` | Member API 3.74: `{"amends":[...]}`, 1 to 50 entries `{orderId, price?, qty?}` (`qty` the new TOTAL quantity), each processed by the engine exactly as `PUT /v1/orders/{orderId}`; non-atomic, array order, a refused entry never stops a later one (send dependent entries in a second message); rows in array order: the order as of `asOfSeq` (can read `REJECTED` 2005 or `CANCELED` `STP_CANCEL`: that level is gone; when the venue could not read the orders back after the batch, the order's last state change among the batch's entries, every row then the order as of `asOfSeq` or later) or `{code,msg,orderId}`; an order named twice: the later entry 2013. One order-budget unit per entry up front after the engine-busy check; an entry on an OTR-blocked market is a 1004 row and the others run. WS twin `orders.batchAmend`; not offered over FIX |
| `POST /v1/orders/batch/cancel` | Member API 3.74: `{"cancels":[...]}`, 1 to 50 entries `{orderId}` or `{clOrdId}`, exactly one; looked up first (an entry naming no open order of yours is a 2014 row, nothing submitted), each other entry processed exactly as `DELETE /v1/orders/{orderId}`; free, never throttled or OTR-blocked; one cancellation batch of yours at a time per venue node (5001 when eight wait). WS twin `orders.batchCancel`; not offered over FIX. Adopting either batch route: re-certify with the Conformance Test Plan's rows X-07 to X-11 before production use |
| `DELETE /v1/orders/{orderId}` | Cancel by venue id. Answers the cancelled order, rendered from the cancel event (`status` `CANCELED`, `cancelReason` `USER_REQUESTED`, `acceptedTs` the cancel's time). An empty id, `DELETE /v1/orders/` with the trailing slash, is 5003 with nothing cancelled since Member API 3.84, never the cancel-all: check the id before you build the path |
| `DELETE /v1/orders?clOrdId=` | Cancel by your reference; send `clOrdId` alone (with `symbol` or `side` beside it: 5003, nothing cancelled) |
| `DELETE /v1/orders?symbol=&side=` | Cancel all, optionally filtered by `symbol` and `side` (`BUY` or `SELL`, any case); without a filter EVERY open order on every market, untriggered conditionals included. Only `/v1/orders` without the trailing slash is this route. Since Member API 3.84 the route reads `clOrdId`, `symbol` and `side` and nothing else: any other query parameter (`clientUid`, `orderId`, a misspelling), a parameter given twice or a `side` other than `BUY` or `SELL` (empty included) is 5003 with `msg` naming it, a request body 2017, and NOTHING is cancelled (before, each was ignored and, without `clOrdId`, the request cancelled everything the remaining filters matched; a repeated `symbol` answered 2001, and with `clOrdId` at most that one order was cancelled). Build the query from the three names only and send no body. No `clientUid` filter: cancel one end client's orders by `orderId` (`POST /v1/orders/batch/cancel`, ids from `GET /v1/orders/open`) |
| `PUT /v1/orders/{orderId}` | Amend price and/or qty (qty-down keeps queue priority). Answers the order read just after the amendment, so it can already carry a later fill, under the amendment's own `asOfSeq` |
| `GET /v1/orders/{orderId}` | Order state, working or terminal |
| `GET /v1/orders/open?symbol=` | Open orders |
| `GET /v1/orders/history?symbol=&from=&to=&limit=` | Closed orders AND rejections (`rejectCode`); `from` and `to` bound `acceptedTs` (section 6). While the store reads `REPAIRING` the rejection rows stop at the watermark and the closed-order rows are not held: they can include orders closed after the hole, above `asOfSeq`, while orders closed inside it are missing |
| `GET /v1/executions?symbol=&from=&to=&limit=` | Fills, with `clOrdId`, `refSource` and (PERP and BARRIER rows, once the venue LEI is configured, Member API 3.70) the venue-leg `uti`. Since member API revision 3.83, while the store reads `REPAIRING`, only the fills at or below the store watermark, which the envelope `asOfSeq` names |
| `POST /v1/rfq` | Open an RFQ on an RFQ-mode market (section 7) |
| `GET /v1/rfq/open?symbol=` | The board: live RFQs you are eligible to quote (anonymous) |
| `GET /v1/rfq/mine` | Your open RFQs with their live quotes (makers anonymous) |
| `GET /v1/rfq/quotes/mine` | Your live quotes across all open RFQs |
| `POST /v1/rfq/{rfqId}/quotes` | Quote an RFQ |
| `POST /v1/rfq/{rfqId}/accept` | Accept one quote and trade |
| `POST /v1/rfq/{rfqId}/cancel` | Withdraw your RFQ (never rate-limited) |
| `POST /v1/rfq/{rfqId}/quotes/{quoteId}/cancel` | Withdraw your quote (never rate-limited) |
| `GET /v1/barriers` | Your live barrier contracts, both roles, marked to the live mark |
| `GET /v1/account` | Balance, equity, IM/MM, available, `imMultiplier`, `feeTier`, fee rates, assets (a `EUR` asset row of a USD-settled venue also carries the informational `reference` block, the ECB-rate USD equivalent of `total`, Member API 3.70; omitted elsewhere) |
| `GET /v1/account/margin` | Same payload (cheap polling alias) |
| `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. Since member API revision 3.83, while the store reads `REPAIRING`, only the rows at or below the store watermark |
| `GET /v1/positions` | Member-level positions |
| `GET /v1/positions/end-clients?symbol=` | Per-`clientUid` sub-positions |
| `POST /v1/margin/preview` | Pre-trade margin cost of hypothetical orders, priced as one basket (1 to 50 entries) |
| `GET /v1/margin/calls?from=&to=&limit=` | Margin-call history |
| `GET /v1/adl/indicator?symbol=` | Your ADL quintile (1 to 5) |
| `GET /v1/adl/events?from=&to=&limit=` | ADL executions affecting you |
| `GET /v1/unwinds` | Liquidation episode reports |
| `GET /v1/stream/cursor` | Recovery cursor and store status |
| `GET /v1/stream/events?afterCursor=&limit=` (or `fromSeq=`) | Sequence-ordered event backfill |
| `GET /v1/reconciliation/end-client-positions?date=&format=` | EOD per-client snapshot (CSV available) |
| `GET /v1/market-segments` / `PUT /v1/market-segments` | Counterparty acceptance |
| `PUT /v1/clients/{clientUid}/identity` | RTS 22 identity record for one `clientUid` (member reference section 6d): upsert, strict body (2017), validation failures 2030 naming the field, immutable audit `revision` per PUT (until the retention floor, revision 3.70), trading-capable credential required (READ_ONLY gives 1005). Since revision 3.70 the body also takes the optional `positionHolderCategory` (the five MiFID II Article 58(4) values, else 2030) and `ultimateParentLei` (ISO 17442, else 2030) on both client types, cleared when omitted; the category is required in practice for a client holding a position on a commodity-derivative market (the venue's Article 58(1)(b) daily position report holds a record without it back). Trading is never blocked on identity data |
| `GET /v1/clients/{clientUid}/identity` | The current record; 2031 (404) when none, or after the venue pseudonymised a natural person's record at the five-year retention floor (revision 3.70). READ_ONLY permitted |
| `GET /v1/clients/identity/missing?from=&to=&limit=` | Your `clientUid`s that traded on MiFIR-scope markets (perpetual and barrier, revision 3.52) in the window with no identity record: the chase list for the T+1 reporting deadline. Envelope `asOfSeq` is the projection watermark. A member whose licensing profile records that it reports its own transactions always receives an empty `clients` array (revision 3.70). All three routes answer 5004/503 on a deployment without the venue's history store |
| `PUT /v1/trader-ids/{traderId}/identity` | Revision 3.53: the natural person (`kind: "PERSON"`, the client identity rules plus a mandatory `branchCountry`) or algorithm (`kind: "ALGO"`, `algoId`) behind a Trader ID, RTS 22 fields 57 to 60. Strict body (2017), validation failures 2030, trading-capable credential required (1005 on READ_ONLY). A path id that begins or ends with whitespace is 2013 (Member API 3.72). 5004/503 on a deployment without the venue's history store |
| `GET /v1/trader-ids/{traderId}/identity` | The current trader identity; 2031 (404) when none. READ_ONLY permitted |

Since revision 3.53 the identity echoes carry `mifirIdScheme` (`LEI`, `NIDN`, `CCPT`, `CONCAT`, the ISO 20022 scheme the identifier is reported under) and, on a PUT whose identifier choice contradicts the RTS 22 Annex II per-country table, a `warnings` array (the sandbox WARN stage until 2026-09-21; the same findings reject with 2030 in the ENFORCE stage, in force on production from the first day and on the sandbox from Tuesday 2026-09-22). Since revision 3.70 the client identity echoes also carry `positionHolderCategory` and `ultimateParentLei` (null when not on file), and a natural person's record pseudonymised by the venue at the retention floor answers 2031 until a new PUT starts a new revision. Member reference section 6d.

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

## 6. Placing an order

```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": "1",
  "reduceOnly": false,                 // perps only (2016 on spot)
  "clOrdId": "a3f1c9d24b7e4f108c6d2e5b",
  "clientUid": "CLIENT-A",
  "leverage": 10,                      // OPTIONAL assertion, validated then echoed
  "quoteCcy": "USD",                   // OPTIONAL assertion
  "reg": {                             // regulatory order-record fields
    "traderId": "TR-12-9F3A21BC"       // Authorised User entering the order (see below)
  },
  "trigger": {                         // conditionals only
    "source": "MARK",                  // LAST | MARK | INDEX
    "price": "60000",
    "direction": "AT_OR_BELOW",        // AT_OR_ABOVE | AT_OR_BELOW
    "limitPrice": "59900"              // *_LIMIT types only
  }
}
```

Key semantics:
- `clOrdId`: your reference, <= 36 chars, unique per member per 24 h. It is also
  the **idempotency key**: resending the identical payload returns the original
  order with `"duplicate": true`; the same reference with a different payload
  rejects with 2009. The `KX-` prefix is reserved (2020). The venue keeps a
  reference for 24 h, or until it releases it to stay inside its retention bound
  of the most recent 4,000,000 references venue-wide, whichever comes first; a
  reference whose order is still open is never released. Retries act in seconds
  and are unaffected, but do not reconcile hours-old state by resending: read it
  back with `GET /v1/orders`.
- `clientUid`: required end-client reporting identifier (2010 if missing).
  It rides every response and notification, including rejections.
- 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 (`traderId`, `investmentDecisionId`, `executionDecisionId`) that begins or
  ends with whitespace rejects with 2013 and a `msg` naming the rule, per entry on
  the batch route, on the RFQ routes too, and as the path identifier of the two
  identity routes. Trim where you build the value; never trim a value you already
  trade under without telling venue operations, the trimmed one is another client.
- MARKET orders execute inside a protection collar; margin is checked at the
  collar price, so a market buy needs slightly more free margin than the touch
  suggests. Empty book gives 2007.
- MARKET + FOK is all-or-none inside the collar: either a full fill set or one
  cancel with `FOK_UNFILLABLE`, never a partial.
- `reg.traderId`: the venue-issued Trader ID of the Authorised User entering
  the order. Default form `TR-{memberId}-{8 hex}`; treat it as an opaque
  string of up to 64 chars. Enforcement is STAGED per member: while your
  member has NO registered Authorised Users, nothing is gated and the field is
  optional (recorded verbatim when sent). From the FIRST registered user,
  EVERY order must carry a registered, ACTIVE Trader ID or it rejects 2021
  (HTTP 400, msg `reg.traderId missing or not a registered Authorised User`)
  before it reaches the engine. The gate 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, never gated on 2021. The `reg` object also carries the
  RTS 24/22 order-record fields; the venue records them verbatim.
- `POST /v1/orders` on an RFQ-mode market always rejects 2022; trade through
  the `/v1/rfq` routes instead (section 7).
- `acceptedTs` names a different instant per surface (member API section 6,
  revision 3.83 states it): on every order frame and stream row, and on every
  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), it is the sequencing time of
  that event; on the open-order reads (`GET /v1/orders/open`, and
  `GET /v1/orders/{orderId}` or the `PUT` answer while the order is open) it is
  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` a
  `FILLED`, `CANCELED` or `EXPIRED` row carries the first acceptance (and
  `closedTs` the terminal event), a `REJECTED` row the rejection time in both.
  Never read it as "the time the order was accepted" without knowing the
  surface.

## 7. RFQ markets and barrier contracts

Some markets trade by request-for-quote instead of a central book: `mode:
"RFQ"` on `GET /v1/instruments`. A requester opens an RFQ, other members
answer with quotes, the requester accepts exactly one. There is no resting
liquidity: `POST /v1/orders` on an RFQ market rejects 2022, and the `/v1/rfq`
routes on an order-book market reject 2023. Three kinds trade this way: a
`PERP` RFQ executes an off-book perpetual trade, a `SPOT` RFQ is prefunded
delivery at the quoted price, and a `BARRIER` market (always RFQ) opens a
knock-out contract.

Ground rules, shared with order entry:

- Create, quote and accept are ORDER messages: order rate bucket (1003),
  TRADE credential (1005), Trader ID gate (2021, on all three), strict bodies
  (2017 naming the unknown field).
- `clOrdId` optional, <= 36 chars, `KX-` prefix reserved (2020 on create and
  quote). `clientUid` required (2010).
- Anonymity is structural and symmetric: makers never learn who requested,
  the requester never learns who quoted. Market segments are enforced
  throughout; an RFQ outside your eligibility set is indistinguishable from
  one that does not exist (2024).
- RFQ is served over REST and the private WebSocket channels only. It is NOT
  offered over FIX.

**`POST /v1/rfq` is idempotent on its `clOrdId` since Member API 3.85.** Like
order entry, a request (and a quote) whose `clOrdId` you already used answers
2009 and opens nothing, from the venue's replay-contract line for that release;
unlike order entry there is no restatement of the original. After a timeout,
resend with the SAME `clOrdId` (signed afresh): a 2009 means the first send
opened the request, read it on `GET /v1/rfq/mine`. 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 (free).

### Opening an RFQ

```json
POST /v1/rfq
{
  "symbol": "BTC-USD-KO",
  "side": "BUY",                       // your side; on a barrier market: holder direction
  "qty": "2",
  "expiresInMs": 60000,                // quoting window; default 60000, 1 s to 24 h (2013)
  "barrier": {                         // BARRIER markets only; else absent (2027)
    "level": "65000",                  // knock-out level; BUY: below mark and entry
    "expiryTs": 1785086400000,         // OPTIONAL contract expiry, unix ms
    "takeProfit": "75000",             // OPTIONAL take profit (rev 3.43); BUY: above mark and entry
    "strictTakeProfit": false          // OPTIONAL (rev 3.45); true = pays only on a TP touch before expiryTs (needs takeProfit + expiryTs <= 24 h)
  },
  "clOrdId": "rfq-req-0001",
  "clientUid": "CLIENT-A",
  "reg": { "traderId": "TR-12-9F3A21BC" }
}
```

Response `data` (also the row shape of `GET /v1/rfq/mine`):

```json
{
  "rfqId": 4210, "symbol": "BTC-USD-KO", "kind": "BARRIER",
  "side": "BUY", "qty": "2",
  "expiryTs": 1785000060000, "createdTs": 1785000000000,
  "barrier": "65000", "contractExpiryTs": 1785086400000, "takeProfit": "75000",
  "strictTakeProfit": false, "premium": "9000", "premiumBps": 9000,
  "clOrdId": "rfq-req-0001", "clientUid": "CLIENT-A",
  "quotes": []
}
```

`strictTakeProfit`, `premium` and `premiumBps` (revision 3.45) appear on every
barrier RFQ view (this response, `GET /v1/rfq/mine`, `GET /v1/rfq/open`, the
WS `rfq` `created` frame) and are absent on perp/spot RFQs. `premium` is the
holder's maximum loss, `|entry - barrier| x qty x contract value x
premiumBps / 10000`; on an open request it is INDICATIVE at the current mark
(the entry is the quoted price), on the accept response and on the contract
it is frozen. `premiumBps` is `10000 - knockOutRebateBps` of the instrument on
a normal contract and its `strictTakeProfitPremiumBps` on a strict one, both
published per BARRIER market on `GET /v1/instruments` (1000 and 5000 on every
KO market listed today, so 9000 above at a 70000 mark).

`expiryTs` ends the quoting window; every live quote dies with it. At most 64
open RFQs per member (2012). The market must be tradable (3003 halted, 3005
no mark yet, 3006 delisted) and an RFQ opens NEW risk, so any reduce-only
state rejects 3002. On perp/spot RFQ markets `barrier` and `contractExpiryTs`
are absent from responses (null-dropping serializer).

### Discovery and your state

| Endpoint | Returns (`data` is an ARRAY on all three) |
|---|---|
| `GET /v1/rfq/open?symbol=` | Every live RFQ you are ELIGIBLE to quote, requester anonymous, your own excluded. Rows carry `quoteCount` (live quotes) and `myQuote` (`{quoteId, price, expiryTs, clOrdId}`, only when you have one). |
| `GET /v1/rfq/mine` | Your open RFQs with `quotes`: each live quote as `{quoteId, price, expiryTs}` ONLY. Makers are anonymous; pick by `quoteId`. |
| `GET /v1/rfq/quotes/mine` | Your live quotes across all open RFQs: `{quoteId, rfqId, symbol, price, expiryTs, clOrdId}`. |

The board is eligibility-filtered per caller, so it cannot be a broadcast
feed: poll `/v1/rfq/open` at a sensible rate on your read bucket. Lifecycle
pushes for your own RFQs, quotes and trades ride the private WebSocket `rfq`
channel (recovery notes below).

### Quoting

```json
POST /v1/rfq/{rfqId}/quotes
{
  "price": "70000",                    // on a barrier market: the ENTRY price
  "expiresInMs": 30000,                // validity; default 30000, 1 s to 24 h (2013)
  "clOrdId": "rfq-mk-0007",
  "clientUid": "HOUSE",
  "reg": { "traderId": "TR-31-0C44D2AA" }
}
```

Response `data`:

```json
{ "quoteId": 9105, "rfqId": 4210, "symbol": "BTC-USD-KO",
  "price": "70000", "expiryTs": 1785000040000, "clOrdId": "rfq-mk-0007" }
```

Key semantics:
- A quote commits you to the RFQ's FULL size: the order-size cap applies
  exactly as it would to a book order of that size (2013): `maxOrderNotional`
  valued at your quoted entry price where the market publishes it, else the
  base-unit `maxOrderQty`.
- The instrument's price band around mark applies (2003): the price is
  negotiated, but the band is a fair-and-orderly backstop.
- ONE live quote per maker per RFQ: a re-quote replaces yours in place, under a
  NEW `clOrdId` since Member API 3.85 (a reference you already used answers
  2009 and replaces nothing, so a stale quote can never come back). Retry a quote
  with the SAME `clOrdId`: a 2009 then means the first send landed (read
  `GET /v1/rfq/quotes/mine`). An RFQ holds at most 64 live quotes (2012).
- Quoting your own member's RFQ: 2026. RFQ unknown, closed, expired or not
  visible to you: 2024. Barrier market: the entry must sit on the correct
  side of the barrier level, and strictly inside any take profit (2027).

### Accepting

```json
POST /v1/rfq/{rfqId}/accept
{ "quoteId": 9105, "reg": { "traderId": "TR-12-9F3A21BC" } }
```

Response `data` (`contractId` on barrier markets only):

```json
{ "rfqId": 4210, "quoteId": 9105, "tradeId": 56230,
  "symbol": "BTC-USD-KO", "side": "BUY", "price": "70000",
  "qty": "2", "contractId": 52,
  "barrier": "65000", "takeProfit": "75000", "contractExpiryTs": 1785086400000,
  "strictTakeProfit": false, "premium": "9000", "premiumBps": 9000,
  "ts": 1785000012345 }
```

Only the requester can accept. Acceptance is atomic: validation, margin on
BOTH parties and execution happen in one sequenced command, and the RFQ
closes with the trade.

**The price band is RE-APPLIED at accept against the CURRENT mark.** A quote
that was inside the band when made but has drifted outside it is stale and
rejects 2003: the venue will not execute a price far off the mark it risks
against. Take a fresh quote. Rejection map at accept:

| Failure | Code |
|---|---|
| Your margin (perp, spot buy, barrier holder) | 3001 |
| Your spot-sell base-asset balance | 3004 |
| Your position cap (notional at the current mark where `maxPositionNotional` is set, else `maxPositionQty`) | 2011 |
| The QUOTING member's margin or limits | 3007. The quote STAYS listed; accept a different one. |
| Your member is reduce-only | 3002 |
| The maker went reduce-only after quoting | 2025 (the quote resolves as gone) |
| Quote unknown, withdrawn or expired | 2025 (HTTP 404) |
| RFQ unknown, closed, expired or not yours | 2024 (HTTP 404) |
| Quote outside the band vs the CURRENT mark | 2003 |

A rejection executes nothing and leaves the RFQ open with its quotes. An
accept that times out is resolved by READING, never by blind resends: on
success the RFQ leaves `GET /v1/rfq/mine` and the trade is in
`/v1/executions`; re-accepting a filled RFQ answers 2024 and changes nothing.

Executions: a perp RFQ trade books positions, P&L, fees and margin exactly
like a book fill, publishes on the public trades feed and in
`GET /v1/executions` flagged `RFQ` (carrying your RFQ's or quote's
`clOrdId`). Because the price is bilaterally negotiated it never moves the
last-trade print, never fires conditional orders and never feeds the circuit
breaker. A spot RFQ settles as prefunded delivery at the quoted price.

### Cancels (always free)

| Endpoint | Effect |
|---|---|
| `POST /v1/rfq/{rfqId}/cancel` | Requester withdraws the RFQ; every live quote dies with it. Returns `{rfqId, status: "CANCELED"}`. |
| `POST /v1/rfq/{rfqId}/quotes/{quoteId}/cancel` | Maker withdraws its quote. Returns `{rfqId, quoteId, status: "CANCELED"}`. 2025 when the quote is not yours or no longer live. |

Like order cancels these consume NO rate-limit token and are never
throttled: a member must always be able to withdraw a price.

### Barrier (knock-out) contracts

A barrier market is its own instrument with its own symbol and its own mark
feed (`BTC-USD-KO` on the sandbox, via the normal `/v1/mark-price` route).
The requester is ALWAYS the holder: BUY = long, knocked out at or below
`barrier.level`, which sits BELOW the entry; SELL = short, barrier ABOVE.
The maker writes the contract and quotes the ENTRY price. Since revision
3.43 the request may also carry `barrier.takeProfit`, a second exit on the
PROFIT side of the entry (BUY: above mark and entry; SELL: below both), so
the two levels always bracket the entry. Invalid terms (wrong side of mark
or entry for either level, `expiryTs` in the past) reject 2027 at create, at
quote and again at accept.

- **Knock-out:** the first mark at or past the barrier terminates the
  contract AT THE BARRIER PRICE, even if the mark gapped straight through: a
  guaranteed stop with no gap risk for the holder.
- **Take profit (optional):** the first mark at or past `takeProfit`
  terminates the contract AT THE TAKE-PROFIT PRICE, gap or no gap, fee-free
  like the knock-out; the holder banks `|takeProfit - entry| x qty`. One tick
  can never cross both levels because they sit on opposite sides of the entry.
- **Expiry:** with `expiryTs` set, the contract settles at mark on the first
  price tick at or past that time, floored at minus the premium and capped at
  the potential profit when a take profit exists; omit it for an open-ended
  contract on a market without `maxContractLifeHours` (a market that publishes
  that field on `GET /v1/instruments`, revision 3.70, requires `expiryTs` at
  most that many hours after the request, else 2027). A STRICT take-profit
  contract that expires without a touch forfeits the whole premium whatever
  the mark.
- **Holder margin = the premium (revision 3.45):** the entry-to-barrier
  distance times quantity times the contract's `premiumBps` share, with IM
  equal to MM: exactly the maximum possible loss. `premiumBps` is frozen at
  the open: `10000 - knockOutRebateBps` on a normal contract (1000 bps rebate
  on every KO market listed today, so 90 percent of the distance),
  `strictTakeProfitPremiumBps` on a strict one (5000 today), 10000 on
  contracts opened before the revision; both instrument fields are on
  `GET /v1/instruments` for BARRIER markets. Thereafter the requirement tracks
  the REMAINING mark-to-barrier distance less the rebate (premium plus
  unrealized P&L, floored at 0), so unrealized loss and released requirement
  cancel tick for tick: the contract can NEVER liquidate its holder (the
  knock-out IS the holder's close-out), and unrealized profit stays reserved
  until the contract closes. At a knock-out the holder loses exactly the
  premium; the writer receives it.
- **Strict take profit (revision 3.45):** `barrier.strictTakeProfit: true`
  buys a cheaper contract that pays ONLY if the take profit is touched before
  `expiryTs`; requires `takeProfit`, an `expiryTs` at most 24 h after the
  request and a market whose `strictTakeProfitPremiumBps` is nonzero, else
  2027 (re-checked at accept). Every barrier view carries the flag.
- **Writer margin:** like a perp on the instrument's risk tiers at current
  notional, bounded by the remaining mark-to-take-profit distance times qty
  when the contract carries a take profit (the smaller of the two applies);
  the writer's P&L is the exact negation of the holder's.
- **Fees:** entry pays normal maker/taker fees on the entry notional (holder
  taker, writer maker). Knock-out, take profit, expiry and every other
  venue-initiated settlement is fee-free. Settlement P&L books in the account ledger as kind
  `BARRIERSETTLE`, zero-sum between holder and writer.
- **Tear-up:** a member entering liquidation has its contracts, both roles,
  torn up at the barrier-clamped mark; its open RFQs are canceled and its
  quotes withdrawn at the same moment. A perp POSITION on an RFQ market is
  liquidated AT MARK against ADL-ranked members holding the opposite side,
  whole position in one step: it arrives as an ADL-flagged trade, no
  bankruptcy required.

**Worked example** (`BTC-USD-KO`, `knockOutRebateBps` 1000): holder BUY,
qty `2`, quoted entry `70000`, barrier `65000`. Distance (70000 - 65000) x 2 =
10000; premium = 10000 x 9000 / 10000 = **9000** = holder margin at entry
(IM = MM). If the mark touches 65000 the contract settles AT 65000 and the
holder loses exactly the premium, **9000**: the reserved margin IS the worst
case, even if the mark gaps through the barrier. At a 68500 mark the holder
shows -3000 unrealized and a 6000 requirement (remaining distance 3500 x 2 =
7000 less the 1000 rebate); loss plus requirement always sums to the original
9000. At a 72000 mark the holder shows +4000 and the requirement GROWS to
(72000 - 65000) x 2 - 1000 = 13000: profit stays reserved until the contract
closes. A STRICT take-profit contract on the same terms would carry a 5000
premium and margin.

Contracts never appear in `GET /v1/positions` and never contribute to open
interest. They live only here:

```json
GET /v1/barriers
{
  "code": 0, "msg": "ok",
  "data": [
    {
      "contractId": 52, "symbol": "BTC-USD-KO",
      "role": "HOLDER",                // HOLDER | WRITER
      "direction": "LONG",             // the HOLDER's direction, both roles
      "qty": "2", "entry": "70000", "barrier": "65000",
      "takeProfit": "75000",           // absent on a knock-out-only contract
      "strictTakeProfit": false,       // rev 3.45
      "premium": "9000", "premiumBps": 9000, // rev 3.45: holder maximum loss, frozen at the open
      "status": "OPEN",                // OPEN here; CLOSED rows under ?status=CLOSED|ALL
      "markPrice": "68500",
      "barrierDistance": "0.051094",   // (mark to barrier) / mark, floored to 6 dp
      "unrealizedPnl": "-3000",        // role-signed: yours in THIS role
      "margin": "6000",                // holder: remaining distance less rebate; writer: min(tier margin, remaining TP distance)
      "expiryTs": 1785086400000,       // absent when open-ended (a market without maxContractLifeHours only)
      "clientUid": "CLIENT-A",
      "openedTs": 1785000012345
    }
  ],
  "asOfSeq": 105210, "ts": 1785000020000
}
```

`GET /v1/barriers?status=CLOSED` (or `ALL`) adds your recently settled
contracts, newest first, kept since the last venue start: the same row shape
minus the mark-derived members, plus `settlePrice`, role-signed
`realizedPnl`, `terminationReason` (`KNOCK_OUT` | `TAKE_PROFIT` | `EXPIRY` |
`TEAR_UP` | `FINAL_SETTLEMENT` | `EARLY_CLOSE`) and `closedTs`. Any other
`status` value is 2013.

**Early close by RFQ (revision 3.44).** The holder of an open contract can ask
its writer to close it: `POST /v1/rfq` with `symbol` (the contract's market)
and `closeContractId` (an OPEN contract you HOLD), and NO `side`, `qty` or
`barrier` (2013 if sent): the venue derives the closing side (a long holder
SELLs, a short holder BUYs), the full quantity and the contract's terms, and
echoes them with `closeContractId` on every RFQ view. `clientUid` may be
omitted (defaults to the contract's holder-side client) or must equal it
(2010). One close request per contract at a time; an id that is not an open
contract you hold on this market is 2035. Only the contract's WRITER sees the
request (its `GET /v1/rfq/open` and its WS `rfq` `created` frame, role
`WRITER`) and only the writer may quote it (anyone else: 2024), at a price
STRICTLY INSIDE the terms, `barrier < price < takeProfit` for a long holder
and mirrored for a short one (2027 otherwise). Your `POST /v1/rfq/{rfqId}/accept`
then CLOSES the contract AT that price: a closing trade flagged
`BARRIER_CLOSE` (maker/taker fees exactly like the entry, holder taker, writer
maker), a `BARRIERSETTLE` zero-sum settlement, `terminationReason`
`EARLY_CLOSE`; closing releases margin, so the only admission is solvency (a
close that costs a party no more than it releases is always admitted, otherwise
its available must stay non-negative afterwards: 3001 holder, 3007 writer); the accept
response carries `contractId` (the closed contract), `closeContractId`,
`settlePrice` and your `realizedPnl` (clamped to [minus the premium, plus the
potential profit]) beside the trade fields. If the contract
knocks out, hits its take profit, expires or is torn up while the close request
is open, the venue cancels the request in the same command (`rfq` `canceled`
frame). Cancelling a close request is free like every cancel.

```json
POST /v1/rfq
{ "symbol": "BTC-USD-KO", "closeContractId": 52, "expiresInMs": 60000,
  "clOrdId": "close-52-1" }
```

### Live updates and recovery

The private WebSocket channels `rfq` (types `created`, `quoted`, `myQuote`,
`quoteCanceled`, `accepted` with your `role`, `canceled`, `expired`; every
frame of a close request carries `closeContractId`, and a close request's
`created` is also delivered to the contract's writer with role `WRITER`) and
`barriers` (`opened`, `knockOut`, `takeProfit`, `expiry`, `earlyClose`,
`liquidation`, `finalSettlement`, with `settlePrice`, role-signed `pnl`,
`takeProfit` when set and `terminationReason` on every close) carry the push
side. Both are GUARANTEED delivery but NOT cursor-backfillable: after a WS
gap frame naming them, recover by re-reading state (`GET /v1/rfq/mine`,
`GET /v1/rfq/open`, `GET /v1/barriers`), not by cursor. Details in the
companion WebSocket manual.

## 8. Errors and retries

Full registry: `KronosX-API-Codes`. The essentials for a REST client:

| Codes | Retry? |
|---|---|
| 1002 clock skew, 1003 rate limit, 5001 engine busy | Yes, with backoff and jitter. **Reuse the same `clOrdId`** on order entry so the idempotency key protects you, and **sign every retry afresh**: since Member API 3.85 a signed write is accepted once, so a resend of the same bytes answers 1002 `request already received`; a 1002 that names `X-RECV-WINDOW` or a timestamp ahead is fixed first (the header, or your clock). A 1003 whose `msg` is `rate limit: repeated engine faults (5005)` (Member API section 4, revision 3.79) follows ten `5005` answers of yours within 60 seconds: sleep `retryAfterMs`, up to 60 seconds, and never resend the commands that answered `5005` 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 with `reduceOnly` true, a barrier early close) still pass while that budget refuses. One exception to "5001 was not accepted", on a clustered deployment (member API section 10.7): a SINGLE write 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, so during a leader change read it back by `clOrdId` on the leader before resending it, or resend it with the SAME `clOrdId` (a batch row's 5001 is never such a write). |
| 5000 internal error | Reads yes. Order entry: reconcile by `clOrdId` (`DELETE /v1/orders?clOrdId=` or `GET /v1/orders/{id}`) BEFORE resubmitting. On a clustered deployment a write whose commit the venue did not confirm within 10 seconds answers 5000 (HTTP 500, `msg` `internal error`, the WebSocket ack too since member API revision 3.83): its outcome is unknown and it may still execute, so read it back on the leader by `clOrdId` or send it again with the SAME `clOrdId`. |
| A batch ROW with 5001, 5000 or 5005 (Member API 3.74: inside HTTP 200, the other rows stand) | 5001: that entry was not accepted, resend it alone (a placement with the same `clOrdId`). 5000: outcome unknown, read the order back on the leader first (a placement by its `clOrdId`) and resend a placement only with the SAME `clOrdId`. 5005: reconcile, never resend it as it was. |
| 5007 history store busy (HTTP 503, since Member API revision 3.73) | Yes, reads only: the reads of the venue's history store (`GET /v1/stream/events`, `GET /v1/stream/cursor`, `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`) answer it when the store did not answer in time, is held after a slow read, or is catching up with the venue. Sleep `data.retryAfterMs` (also `Retry-After`), then retry the SAME read with backoff, with the same cursor on a stream page (never advance your cursor on a refusal); after repeated 5007 on the same read, narrow it (a smaller `limit`, a later cursor or `from`, no `symbol`). Nothing changed on your account. On the event stream routes the same HTTP 503 also carries 5004 (this deployment has no event store: permanent): branch on the code, never on the status. |
| 5005 command failed | No, never unchanged: the venue is deterministic, so an identical resubmission fails identically. On order entry it arrives as `data.rejectCode` inside a 200, and the rest of `data` reports what the command DID achieve before it stopped (an `orderId` there is a real order, any `executions` really happened). Reconcile against it and `GET /v1/orders/open`, cancel what you did not want, then send a DIFFERENT request if you still need the position. Report the `clOrdId` to venue operations. |
| 3003 halted, 3005 no mark price, 5002 venue halted | Not a retry matter. Wait for the state change (see `GET /v1/status`). |
| 1001 auth, 2001 to 2035, 3001, 3002, 3004, 3006, 1005 | No. Fix the cause. 2021 means register or attach a valid Trader ID; 2003 on an RFQ accept means the quote went stale against the current mark, so take a FRESH quote, never resubmit the old one. 2030 means an RTS 22 identity body failed validation (`msg` names the field); 2031 clears once the identity record is PUT, not by resending the GET. |
| 3007 counterparty margin (RFQ accept) | Not by repeating the same accept: the QUOTING member no longer passes margin or limits. The quote stays listed; accept a DIFFERENT quote. |
| 5030 NOT_LEADER | Clustered deployments only, among them the clustered sandbox of member API revision 3.83 from the date venue operations announce (HTTP 503): a node that is not the leader answered a request that is not a `GET` under `/v1` (a read-only `POST` such as `POST /v1/margin/preview` included) before the route ran, and never forwarded it, so the command 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; while no leader is known `leaderId` is 0 and `leader` null. The node closes the connection after its answer (`Connection: close`, GOAWAY on HTTP/2), so your next request opens a new one, which the entry host sends to the leader. It is a redirect, never a rejection and never a backoff: accept `leader` only when it equals the entry host you configured, then send the request again there with the SAME `clOrdId`; 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; if it is null, back off briefly and retry your entry host. Never count a 5030 as a reject. Member API section 10.7. |
| 4001 version conflict | Yes, after re-reading the current version. |

**Never blind-retry a POST /v1/orders with a fresh `clOrdId`.** A timeout does
not mean the order was not accepted; retrying with the same reference is safe,
retrying with a new one can double your position. Branch on the `code` of an
HTTP 503, never on the status alone: `5001`, `5004`, `5007` and `5030` share
it.

**During a leader change** (clustered deployments, member API section 10.7)
only a `5030` proves that the command did not take effect. A single write
answered `5001`, a `5000`, a timeout and a request whose connection ended
without an answer may still execute on the new leader: read the order back on
the leader by `clOrdId` (`GET /v1/orders/open`, `GET /v1/orders/history` or the
event stream of section 10) before you decide, or send it again with the SAME
`clOrdId`. A leader change pulls no resting order; re-quote or cancel what you
no longer want once you are on the leader.

**`2019` on a backfill cursor.** A cursor is 30 ASCII digits: `seq` (19
digits, a signed 64-bit integer, 0 to 9223372036854775807), `eventIndex` (10
digits, a signed 32-bit integer, 0 to 2147483647: the position of an event
among the events of one engine command, restarting at 0 for every `seq`) and
`leg` (1 digit). A field above its range answers `2019`, and since member API
revision 3.83 its `msg` names the field and the range; anything that is not 30
ASCII digits answers `afterCursor must be 30 numeric characters`. Store the
cursors the venue returns as strings, pass them back unchanged, and branch on
the code.

## 9. Rate limits

Two INDEPENDENT token buckets per MEMBER, shared by every API key the member
holds (up to 5 per scope): **6000 reads/min** and a **40000/min order budget**.
Order placements, amendments and every entry of a placement or amendment batch
consume the order budget, one unit each; cancels, the entries of a cancellation
batch included, are never throttled. Order-budget traffic does not consume a
read token. Holding several keys does not multiply the budget. A placement batch
(`POST /v1/orders/batch`) and an amendment batch (`POST /v1/orders/batch/amend`,
Member API 3.74, charged after the engine-busy check) are charged one unit per
entry, up front, all or nothing: a denied batch answers 1003 with
`retryAfterMs` and consumes nothing, and the units of entries refused after the
charge stay spent. Both are full-capacity buckets, so after a
quiet period the whole minute's allowance is available at once (sized for
liquidation bursts), 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. Breach gives 1003.

RFQ traffic follows the same split: `POST /v1/rfq`, quote and accept are
order messages against the 40000/min order bucket; the RFQ and barrier GET
routes are reads against the 6000/min read bucket; the two RFQ cancel routes
consume neither and are never throttled.

**Reads of the venue's history store (Member API revision 3.73).** Beside the
read bucket, the event stream routes (`GET /v1/stream/events`,
`GET /v1/stream/cursor`, `GET /v1/margin/calls`) and the history routes
(`GET /v1/executions`, `GET /v1/orders/history`, `GET /v1/account/ledger`,
`GET /v1/adl/events`, `GET /v1/reconciliation/end-client-positions`) carry four
technical bounds on each node of the venue that serves you: how many reads of
yours of each of the two groups run at once, a budget of these reads in flight
on the node that every member shares, how long one read may run, and a yield to
the venue's own writes to the store. The event stream group covers every stream
reader of yours, all keys and all filters together. A read beyond the in-flight
bound or the shared budget waits briefly for its turn, then answers 1003 with a
SUGGESTED `retryAfterMs`, not an exact refill: a repeated 1003 while other reads
of yours of the group are in flight means your reads overlap, so serialise your
readers; one while no other read of yours of the group is in flight comes from
the shared budget, which serialising cannot help, so back off. A read that
exceeded the time bound answers 5007 (section 8). Keep one request in flight
per stream, wait for its answer before you send the next, and set your client
timeout to at least 15 seconds.

Separately, the **order-to-trade regime** (RTS 9) counts order messages against
trades over a sliding 60-minute window; a block gives 1004 on new orders and
amendments only. Cancels are never blocked.

## 10. What to build

1. **Signer** reproducing the section 3 vector exactly.
2. **HTTP client** with envelope handling, decimal parsing, the retry matrix,
   and a single serialization point so signed bytes equal sent bytes.
3. **Reference-data cache** from `GET /v1/instruments`, refreshed at startup,
   periodically, after every announced date (a re-listing, a fee change, a
   sandbox reset) and on any unexpected `2001`, `2002`, `3006` or `DELISTED`
   status; all rounding driven from it, and order entry vs the `/v1/rfq`
   surface routed by each instrument's `mode`. `venueEvents` carries
   trading-state changes only, never a listing or a parameter change (a
   re-listing shows there only as the `STOP_TRADING` with reason `DELISTING`
   of the old market), and the WebSocket `status.{symbol}` channel also repeats
   a market's unchanged status when the venue edits its configuration: treat
   either as a prompt to re-read, never as the source of a value.
4. **Order state machine** keyed by `clOrdId` plus venue `orderId`, handling
   NEW, PARTIALLY_FILLED, FILLED, CANCELED, EXPIRED, REJECTED, PENDING_TRIGGER,
   plus **unsolicited** terminal states from venue-initiated closes.
   `PARTIALLY_FILLED` appears on the state reads only: on the WebSocket
   `order` channel and its event stream rows every frame before the terminal
   one reads `NEW` (`PENDING_TRIGGER` before a conditional triggers), and the
   partial fills arrive on the `execution` channel.
5. **Sub-position ledger** per `clientUid`, reconciled daily against
   `GET /v1/reconciliation/end-client-positions`.
6. **Recovery**: even a REST-only integration should page
   `GET /v1/stream/events` from its resume point at startup to prove no
   activity was missed while your process was down. Persist, per filter, the
   `nextCursor` of your last page and the cursor of the last event you
   processed, and resume from the ordinal maximum of the two; stop a paging
   loop on `hasMore` false or `count` 0, never on `nextCursor` equal to the
   `afterCursor` you sent (since member API revision 3.73 a last page answers a
   position at the store watermark, valid only with the same `channels` and
   `symbol`, on an environment from the date venue operations announce for it
   there; before that date a last page answers the cursor of its last event,
   else the cursor you sent). `store.status` `REPAIRING` means the store of
   the node that answered has a recorded hole: pages stop below it, so keep
   polling from your last `nextCursor` without skipping it, recover the state
   you need from the state reads meanwhile, and alert your operations. It
   starts at every leader change of a clustered venue (the sandbox included),
   no timeout ends it and the venue sets no maximum duration; `lagSeq` then
   counts from the start of the hole to the engine head (member API section
   11.3).

## 11. Acceptance tests

1. Signer matches the published vector.
2. Place a far-from-market LIMIT GTC; assert `NEW`. Cancel it; assert
   `CANCELED` with `USER_REQUESTED` and your `clOrdId` and `clientUid` echoed.
3. Resend an identical place payload; assert `duplicate: true` and the SAME
   `orderId`. Resend the reference with a different payload; assert 2009.
4. Send an order with an invented extra field; assert 2017 naming the field.
5. MARKET + FOK larger than visible depth; assert a single cancel with
   `FOK_UNFILLABLE` and zero partial fills.
6. Unknown symbol; assert 2001 and that your `clOrdId` is echoed in `data`.
7. Read `GET /v1/account`; assert you handle the ARRAY shape and the `MM` key.
8. Restart your process; page `/v1/stream/events` from the stored resume point
   and assert the merged log has no gaps and no duplicates, and that the loop
   ends on `hasMore` false.
9. RFQ round trip: create an RFQ on the sandbox RFQ market, quote it from a
   SECOND membership's credential (self-quoting rejects 2026), accept the
   quote; assert the accept response carries `tradeId`, the fill appears in
   `GET /v1/executions` flagged `RFQ` with your `clOrdId`, and (barrier
   market) the contract appears in `GET /v1/barriers`.
10. `POST /v1/orders` on an RFQ-mode market; assert 2022. `POST /v1/rfq` on an
    order-book market; assert 2023.
11. Stale quote: obtain a quote, wait until the mark has drifted far enough
    that the quoted price sits outside the price band (the sandbox mark
    moves on its own), then accept; assert 2003 and that the RFQ is still
    open with the quote still listed.
12. Leader change, at the leader hand-over venue operations stage on the
    clustered sandbox at an agreed time (Conformance Test Plan I-05): a write
    answered 5030 is sent again on your configured entry host with the SAME
    `clOrdId`, a cancel at once; assert the 5030 never counts as a reject or
    stops your trading, that a `leader` other than your entry host is never
    followed, and that a write answered 5001 or 5000 meanwhile is read back by
    `clOrdId` before any resend.

## 12. Common mistakes

- Signing a re-serialized body (key order or whitespace differs) instead of the
  transmitted bytes. Symptom: intermittent 1001.
- Treating `code: 0` as "order accepted" without checking `data.status`.
- Using floats for prices, then failing tick alignment with 2002.
- Assuming `GET /v1/account` returns an object.
- Retrying order entry with a new `clOrdId` after a timeout.
- Hard-coding tick/lot or the instrument list.
- Polling `GET /v1/orders/open` in a tight loop instead of consuming the
  WebSocket `order` channel; you will hit 1003 and still be slower.
- Retrying `POST /v1/rfq` with a NEW `clOrdId` after a timeout. Since Member API
  3.85 RFQ creation is idempotent on its `clOrdId` like order entry (a reuse
  answers 2009 and opens nothing, from the venue's replay-contract line for that
  release; there is no restatement of the original): retry with the SAME
  reference, read `GET /v1/rfq/mine` on a 2009, and before that line reconcile
  with `GET /v1/rfq/mine` and cancel a duplicate for free.
- Resending a signed request byte for byte (an HTTP library's automatic retry):
  1002 `request already received` since Member API 3.85. Sign every attempt.
- Treating an RFQ quote as firm until its expiry. The price band is
  re-applied against the CURRENT mark at accept, so a stale quote rejects
  2003: take a fresh one.
- Looking for barrier contracts in `GET /v1/positions`. They are bilateral
  and live only in `GET /v1/barriers`.
- Omitting `reg.traderId` after your member registers its first Authorised
  User: every order, RFQ create, quote and accept then rejects 2021.
- Treating a `5030` as a rejection, sending credentials to a `leader` that is
  not your configured entry host, or pinning a node address: the hint is the
  entry host, and the entry host always leads to the leader.
- Reading a single write's `5001`, a `5000` or a timeout during a leader change
  as "not executed": reconcile by `clOrdId` on the leader first.
- Caching `tickSize` or `qtyStep` across an announced re-listing, or keying
  instruments on anything but the symbol.
