# MTS Toolbox

Verification tools for trading systems — one app per venue and component, each with an **oracle** that is
independent of the code under test and a stated **provenance**. Built by [Zhendong Qin](https://priorstates.com/mts/).

Tools: **`cme-md-verifier`** (MDP 3.0 feed handler, market-by-price and market-by-order: is your book right
after loss, bursts, duplicates, reordering, a late join), **`cme-oe-verifier`** (iLink 3 order gateway: the
AutoCert+ session cases, and the order cases — partial fills, the cancel/fill race, replace rejects,
exchange-initiated cancels — against the venue's order truth) and **`cme-dc-verifier`** (Drop Copy 4.0 MSGW
client: logon, gaps and resends both ways, logout and re-logon, heartbeat lapse); **`nsdq-md-verifier`** / **`bx-md-verifier`** / **`psx-md-verifier`** (Nasdaq, BX and PSX
TotalView-ITCH 5.0 over MoldUDP64: loss, bursts, duplicates, reordering, a late join, recovery by retransmission) and
**`nsdq-oe-verifier`** / **`bx-oe-verifier`** / **`psx-oe-verifier`** (OUCH 4.2 over SoupBinTCP: the dropped connection with replay on re-login, a heartbeat lapse,
rejects, and the order cases); **`nyse-md-verifier`** / **`arca-md-verifier`** / **`amex-md-verifier`** / **`nsx-md-verifier`** / **`chx-md-verifier`** (XDP Integrated Feed: loss, bursts,
duplicates, reordering, a late join recovered from the refresh service and retransmissions) and **`nyse-oe-verifier`** / **`arca-oe-verifier`** / **`amex-oe-verifier`** / **`nsx-oe-verifier`** / **`chx-oe-verifier`** (Pillar FIX: the dropped connection with the sequence continued and a ResendRequest served with
PossDup replay, a heartbeat lapse, rejects, and the order cases); **`bzx-md-verifier`** / **`edgx-md-verifier`** / **`byx-md-verifier`** / **`edga-md-verifier`** / **`cfe-md-verifier`** (Cboe
Multicast PITCH: loss, bursts, duplicates, reordering, the Gap Request Proxy, a late join by spin) and **`bzx-oe-verifier`** / **`edgx-oe-verifier`** / **`byx-oe-verifier`** / **`edga-oe-verifier`** /
**`cfe-oe-verifier`** (BOE v2: the dropped connection with the Unit Sequences replay on re-login, a heartbeat lapse,
rejects, and the order cases — the CFE pair on the futures contract; each exchange from its own build of the one Cboe source, the four equity exchanges sharing PITCH and BOE); **`iex-md-verifier`** (IEX DEEP v1.08 over IEX-TP: loss, bursts, duplicates, reordering, the Gap Fill Server, a late join from DEEP SNAP) and **`iex-oe-verifier`** (IEX FIX v3.11: the dropped session with the PossDup resend, a heartbeat lapse, rejects, and the order cases); **`memx-md-verifier`** (MEMOIR Depth v2.0 over MEMX-UDP: loss, bursts, duplicates, reordering, a late join replayed through the MEMX-TCP gap-fill server) and **`memx-oe-verifier`** (MEMO SBE v2.0 over MEMX-TCP: the dropped connection with the stream replay on re-login, a heartbeat lapse, rejects, and the order cases); **`miax-md-verifier`** (MIAX Pearl Equities DoM 1.3 over MACH: loss, bursts, duplicates, reordering, the retransmission interface, a late join from the Order Book Refresh) and **`miax-oe-verifier`** (MEO 2.7 over ESesM: the dropped connection with the per-engine replay on re-login, a heartbeat lapse, rejects, and the order cases); **`cta-sip-verifier`** and **`utp-sip-verifier`** (the consolidated tapes — CQS/CTS Pillar v2.11b and UQDF/UTDF v4.1: the transport cases plus LULD bands, halts, off-exchange prints, corrections and a market-wide circuit breaker, the National BBO checked at every boundary); **`coinbase-md-verifier`** (Coinbase Exchange
WebSocket level2: the snapshot and l2update batches, a dropped socket, a dead socket, a 400-change batch, 20 ms batches)
and **`coinbase-oe-verifier`** (Coinbase Exchange FIX 4.2: the dropped session with cancel-on-disconnect and no resend, a
dead session, a TestRequest, rejects, and the order cases); **`kraken-md-verifier`** (Kraken WebSocket v2 book: the
checksum, the depth-window rule, a lost update, a dropped and a dead socket) and **`kraken-oe-verifier`** (Kraken REST +
the executions channel: the executions socket dropped with fills in the gap, REST unavailable, refusals, and the order
cases); **`binance-md-verifier`** (Binance depth stream + REST snapshot by the update-id recipe: a dropped diff, a
stale snapshot, a dropped and a dead socket) and **`binance-oe-verifier`** (Binance WebSocket API with the user data
stream subscribed on it: the subscription's connection dropped with fills in the gap, a subscription the venue ends,
the API socket dropped, a rate limit, refusals, and the order cases); **`okx-md-verifier`** (OKX v5 books chained by
seqId / prevSeqId: a lost update, a sequence reset, the idle heartbeat, a dropped and a dead socket) and
**`okx-oe-verifier`** (OKX signed REST + the orders channel: the private socket dropped with fills in the gap and no
snapshot to come back to, a rate limit, refusals, the cancel accepted with sCode 0 that loses to a fill, and the order
cases); **`hyperliquid-md-verifier`** (Hyperliquid WebSocket l2Book: every message a full snapshot, a dropped and a dead
socket, a quiet market against the 60 s idle rule, an ask side that thins to nothing) and **`hyperliquid-oe-verifier`**
(Hyperliquid signed /exchange actions against a venue that recovers the signer — the nonce rules, the user subscriptions
dropped with fills in the gap, the address-based rate limit, refusals incl. the post-only rejected at placement, and the
order cases); **`dydx-md-verifier`** (dYdX v4 indexer v4_orderbook: the message_id gap, the two sides out of order —
the newer update wins —, a dropped and a dead socket) and **`dydx-oe-verifier`** (dYdX v4 Cosmos transactions the venue verifies: the short-term order rules, the
subaccounts socket dropped with fills in the gap, the node down, expiry at the goodTilBlock, refusals, and the order
cases); **`injective-md-verifier`** (Injective's book per block from the LCD on the Tendermint NewBlock trigger: a silent
block stream, a lagging LCD node, a 503, a chain halt) and **`injective-oe-verifier`** (Injective Cosmos transactions the venue verifies — the Ethereum-style key, the mempool sequence rule, the fee — the message's outcome only the tx result tells, the EndBlocker's post-only failure on the chain stream, the chain-stream socket dropped with fills in the gap, the node down, the account's sequence jumping, expiration blocks, and the order cases); **`trace-tape-verifier`** (the FINRA TRACE tape, BTDS 2.1 over MoldUDP64 from the public specification: every print with the size capped as FINRA caps it, cancels and corrections by trade id, the issue's high / low / last by the sale-condition matrix and the Change Indicator, as-of and conditioned prints, halts, the close of the day, lost / duplicated / reordered packets, a quiet spell, a restart mid-day). Each ships with a reference client that passes it. **`btec-md-verifier`** / **`btec-oe-verifier`** / **`btec-dc-verifier`** (BrokerTec on CME Globex — US Treasury actives: the same MDP 3.0, iLink 3 and Drop Copy 4.0 procedures on the venue's fixed-income shape — definitions as template 57, the 1/256 · 1/128 · 1/64 tick ladder, $1mm lots, ISIN-keyed orders — verified against the desk's own BrokerTec handler, gateway and drop-copy client).
Not in the download: benches built from a venue specification that is not public (Tradeweb's dealer-to-client lane). They are
delivered as a service to firms onboarded with the venue — the launcher names the venue and nothing else.

Site: https://priorstates.com/mts/toolbox/

---

## 1 · Set up (10 minutes)

**Requirements.** A Linux host with Docker (`--net host` is needed: the venue is multicast and your gateway
connects to a port). Docker Desktop on macOS / Windows does not give containers host networking — use a Linux
box or VM. Your code (the handler or gateway under test) runs on the same host, or on another machine on the
same LAN if you set the venue interface (below).

```
docker load < mts-toolbox-0.8.tar.gz                     # loads mts-toolbox:0.8 and mts-toolbox:latest
alias mtsx='docker run --rm -it --net host -v mts-toolbox-data:/data/toolbox -v $PWD:/in mts-toolbox'
mtsx doctor                                              # multicast on the interface, ports free, image ok
mtsx list                                                # the tools and their cases
mtsx reference cme-md-verifier clean                     # our reference handler against the venue → the report (≈1 min)
```

If `reference` prints `## md · clean — PASS`, the host is fine: the venue publishes, a client receives it, the
report works. `-v mts-toolbox-data:/data/toolbox` keeps run data between commands; `-v $PWD:/in` lets you
submit files from the current directory.

**Venue interface.** The venue sends multicast on `127.0.0.1` by default (your code on the same host joins
the groups on the loopback interface). For a client on another machine, run every command with
`-e CME_IFACE=<this host's LAN IP>` and have the client join on its own LAN interface.

---

## 2 · Test your feed handler (`cme-md-verifier`)

**What the venue gives you.** MDP 3.0, three instruments, market depth 10:

| | |
|---|---|
| incremental | `239.203.56.1:17925` — `MDIncrementalRefreshBook46` (+ `TradeSummary48` in the same packet on trades); one packet = one match event; 12-byte packet header (MsgSeqNum, SendingTime) |
| snapshot | `239.203.56.2:17926` — `SnapshotFullRefresh52`, one instrument per packet, every second; packet seq restarts at 1 each cycle; `LastMsgSeqNumProcessed`, `TotNumReports` = 3, `RptSeq`, `TransactTime` set |
| instruments | ESZ6 = 500001 (tick 0.25) · ZNZ6 = 203500 (tick 1/64) · CLZ6 = 500003 (tick 0.01); prices as mantissa × 10⁻⁹ |

**What you give the tool: the witness.** A file your handler writes as it runs — nothing is entered by hand.
One JSON line per instrument the packet touched, appended at every event boundary (about 30 lines a second at
this venue's rate; a 40 s case is ~1,300 lines), with the book it holds at that moment. You submit the file once:

```
{"seq": 1042, "secid": 500001, "bid": [[5500.25, 32], [5500.00, 9]], "ask": [[5500.50, 39], [5500.75, 6]]}
```

`seq` is the packet sequence number of the incremental whose processing this boundary ends (the venue's truth
is keyed by the same number); `bid`/`ask` are `[price, qty]` best first, up to 10 levels. A book you just
rebuilt from the snapshot channel carries `"src": "snapshot"`. Write nothing while you are recovering and
publishing nothing. Full contract: `mtsx contract cme-md-verifier`.

**How to produce it — 15 lines in your handler.** Your handler already has the moment: the end of an
incremental packet (`EndOfEvent`, or simply "packet applied"). There, for each instrument the packet touched,
write one line with the packet's MsgSeqNum and that instrument's book. Two helpers do the formatting, and a
complete example handler shows the whole shape:

```
mtsx examples /in                     # copies to ./mts-toolbox-examples/:
  witness/witness_writer.hpp          #   C++: WitnessWriter w("witness.jsonl"); w.line(seq, secid, bids, asks);
  witness/witness_writer.py           #   Python: same call
  py_handler/mdp3_witness_handler.py  #   a complete independent MDP 3.0 handler (~200 lines, stdlib) that passes the tool
mtsx example cme-md-verifier clean    # runs that example handler through a case end to end — see it pass, then copy it
```

If you cannot edit the handler, a sidecar that reads wherever it publishes its book (shared memory, a socket,
a log) and calls the same `line()` works too; and with the MCP server your coding agent writes this adapter.

**Procedure — one case:**

1. Start the venue for the case in one terminal. It prints the endpoints and runs until Ctrl-C (or for N seconds if you give N):
   ```
   mtsx venue cme-md-verifier clean
   ```
2. Start your handler, pointed at the groups above, writing `witness.jsonl` as above.
   Let it run the case's length (`mtsx list` shows it — 40–90 s); stop your handler.
3. Ctrl-C the venue. Then submit the witness and get the verdict:
   ```
   mtsx submit cme-md-verifier clean witness /in/witness.jsonl
   mtsx report cme-md-verifier clean
   ```
4. Read the report: one row per expectation, pass/fail, what happened, evidence lines. Fix, repeat.

**Order of cases:** `clean` first (your handler joins a running session — it must build its book from the
snapshot channel, not from incrementals against an empty book), then `drop_single`, `gap_burst`,
`duplicates`, `reorder`, and last `drop_sustained` (a broken channel: the handler may go quiet, but must never
publish a wrong book). Each case's faults and expectations: `mtsx list`.

**Market-by-order (`mbo_clean`, `mbo_gap_burst`, `mbo_duplicates`, `mbo_reorder`).** The same venue with the
orders behind every level: the 46 carries `NoOrderIDEntries` (OrderID, MDOrderPriority, MDDisplayQty,
`ReferenceID` → the MBP entry of the order's side and price, OrderUpdateAction New/Update/Delete), and the
MBO snapshot channel `239.203.56.3:17927` carries `SnapshotFullRefreshOrderBook53` per instrument per second.
Recover from that channel in these cases (the 52 channel is not the oracle for orders). Add to each witness line
your order-level book, `"orders": {"bid": [[order_id, price, qty, priority], …], "ask": […]}`, best price first
then priority; the report compares `[id, price, qty]` per side as a set at the same sequence, and still checks
your levels (they must be the aggregation of your orders). The Python example handler does it with `--mbo`
(`mtsx example cme-md-verifier mbo_clean`).

Your handler's log is optional (`mtsx submit cme-md-verifier <case> handler_log /in/md.log`); with it, the gap
checks read your `GAP` / `Recovery` lines; without it they are judged from the witness.

**Drive the feed yourself — the `manual` case.** The scripted cases run on a timer. When you are debugging you
want to decide *when* things happen: start the venue with no faults, watch your handler settle, then inject one
fault at a time and mark the phases. The venue has a control channel for that (`mtsx ctl`, or `venue_control`
over MCP):

```
mtsx venue cme-md-verifier manual            # terminal 1: no faults until you say so; runs until Ctrl-C
mtsx ctl cme-md-verifier manual mark baseline      # terminal 2, while your handler runs
mtsx ctl cme-md-verifier manual drop 40            # the next 40 packets are lost — a burst outage, now
mtsx ctl cme-md-verifier manual mark after-burst
mtsx ctl cme-md-verifier manual pause              # the market stands still (nothing published, the truth does not move)
mtsx ctl cme-md-verifier manual resume
mtsx ctl cme-md-verifier manual dup 5 | reorder 2 | drop_every 25 | rate 20 | snapshot off | stats
```

| command | effect |
|---|---|
| `pause` / `resume` | stop / restart publishing (sequence numbers do not advance while paused) |
| `drop N` · `dup N` · `reorder N` | the next N packets: lost · sent twice · held behind their successors |
| `drop_every N` · `dup_every N` · `reorder_every N` | periodic faults from now on (0 = off) |
| `rate MS` | packet interval in ms (0 = the default, 100) — `rate 10` is a 10× burst |
| `snapshot on|off` | the snapshot channel — turn it off to see what your recovery does without it |
| `mark LABEL` | a phase marker: the report adds a per-phase table (witness lines, wrong books, packets dropped per phase) |
| `stats` | counters and the next sequence number |

Ctrl-C the venue, submit the witness, `report` as usual: the verdict is on everything published, and the phase
table tells you which of your moves broke the book.

---

## 3 · Test your order gateway (`cme-oe-verifier`)

**What the venue gives you.** A Globex-shaped iLink 3 acceptor (FIXP / SBE) on `tcp://<host>:44821`, plus the
same MDP 3.0 incremental feed on `239.203.56.1:17925` for your trader's market data. Any access key and firm
are accepted; PartyDetails pre-registration is accepted; instruments as above (ES segment 54, ZN 82, CL 80).

**What you give the tool.** Order flow. The session cases need a client that **keeps sending orders while its
acks are withheld** — the >2,500-message gap needs ~2,600 outbound messages from the venue, so your trader must
send steadily (our reference sends IOC limit orders far from the touch at 15/s; a market maker that waits for
acks stalls and the gap never forms). Your gateway's log is optional but adds the client-side checks
(`mtsx contract cme-oe-verifier` lists the log lines it looks for).

**Procedure — one case:**

1. Start the venue for the case with its duration (from `mtsx list`):
   ```
   mtsx venue cme-oe-verifier gap_gt_2500 150
   ```
2. Connect your gateway to `:44821`, start your order flow, keep it running for the whole duration. The venue
   plays the scenario (withholds, terminates, rejects, stops keepalives) on its own schedule.
3. When the time is up the venue stops and prints the report, judged from its own log. Optionally add your
   gateway log and re-run the report:
   ```
   mtsx submit cme-oe-verifier gap_gt_2500 client_log /in/gw.log
   mtsx report cme-oe-verifier gap_gt_2500
   ```

**Order of cases:** `clean`, `rejects`, `keepalive_lapse`, `terminate_reconnect`, `multiple_gaps`,
`realtime_during_resend`, `gap_gt_2500`; then the order cases below.

**The order cases — `order_flow`, `partial_fills`, `cancel_fill_race`, `replace_rejects`, `unsolicited_cancels`.**
These judge your gateway's *order state*, not the session. The venue writes an order truth (every execution report
it sent, with the order's state after it) and you submit your **order witness**: `orders.jsonl`, one JSON line per
order, the last line per ClOrdID counts —

```
{"cl": "<ClOrdID>", "status": "new|partial|filled|cancelled|rejected|replaced", "qty": 5, "px": 5501.25, "cum": 2}
```

Your order keeper already holds this: dump it at the end of the run. Or write it from your gateway's log —
`examples/order_witness/gw_witness.py` does that for BTS2's log (`mtsx examples /in` copies it), and is the shape
your coding agent writes for yours. Use an order flow with the whole lifecycle (resting orders that fill, a
cancel/replace, cancels); our reference drives one at 1 cycle/s plus three market makers.

```
mtsx venue cme-oe-verifier partial_fills 60         # connect your gateway, run your order flow for 60 s
python3 gw_witness.py gw.log > orders.jsonl          # or dump your order table
mtsx submit cme-oe-verifier partial_fills orders /in/orders.jsonl
mtsx report cme-oe-verifier partial_fills
```

The checks: every venue order known, status + cum agree, fills booked in full, replaces under the new ClOrdID
with CumQty carried over, partially filled orders with the venue's cum, an order whose cancel lost the race to a
fill `filled` not `cancelled`, a rejected replace leaving the original standing, an exchange-cancelled order
`cancelled`; and, from the venue's side alone, `stale_cancels == 0` — no cancel or replace for an order the venue
closed more than 2 s earlier (the in-flight race is counted separately and allowed).

**Drive the acceptor yourself — the `manual` case.** As for the feed: start `mtsx venue cme-oe-verifier manual`,
connect your gateway and order flow, then from another terminal (`mtsx ctl cme-oe-verifier manual …`) or the GUI:

| command | effect |
|---|---|
| `withhold N [burst]` | withhold the next N application messages (served on RetransmitRequest); `burst` hides the gap in `Sequence.NextSeqNo` until the window closes — the >2,500 shape |
| `withhold_every N M` · `release` | N withheld every M messages from now · stop withholding |
| `reject N` · `reject_every N` · `reject_text T` | reject the next N orders (ExecutionReportReject 523) · every Nth · the reject text |
| `terminate [code]` | Terminate 507 now; your gateway must re-establish |
| `lapse MS` | no keepalives for MS ms; your gateway should send Sequence(KeepAliveIntervalLapsed) and keep the session |
| `interleave on\|off` · `retransmit_max N` | a real-time message inside the next retransmission · messages per RetransmitRequest (CME: 2500) |
| `partial N` · `fill_on_cancel N` · `replace_reject N` · `unsolicited_cancel N` | the order scenarios for the next N: a half fill · the cancel loses to a fill · the replace is rejected · the venue cancels the new order |
| `mark LABEL` · `stats` | a marker line in the venue's log · counters and the next sequence number |

---

## 3b · Test your drop-copy client (`cme-dc-verifier`)

**What the venue gives you.** A Drop Copy 4.0 (MSGW) server on `tcp://<host>:44803`: FIX 4.2, any SenderCompID /
access key / HMAC signature accepted, the Logon answered with your HeartBtInt, sequence numbers kept across
connections, ResendRequest served from a store (PossDupFlag=Y replay, SequenceReset-GapFill for admin messages),
the venue's own ResendRequest when your side is ahead. The stream is the encapsulated `35=n` with the embedded
`35=8` (New, Cancel, Fill, Reject) for the venue's own order flow (5 orders/s across ESZ6 / ZNZ6 / CLZ6) and for
anything your gateway sends on the iLink 3 port `:44821`.

**What you give the tool.** Nothing but a logged-on client: the venue sees every session message you send — Logon,
ResendRequest and its range, GapFill, TestRequest, the Logout confirmation, the new Logon — and judges from that.
Your client's log is optional (`mtsx contract cme-dc-verifier` lists the lines it looks for).

**Procedure — one case:**

1. `mtsx venue cme-dc-verifier gap_resend 40`
2. Log your client on to `:44803` and leave it for the whole duration; the venue plays the scenario on its own schedule.
3. When the time is up the venue stops and prints the report. Optionally `mtsx submit cme-dc-verifier gap_resend client_log /in/dc.log`
   and `mtsx report cme-dc-verifier gap_resend`.

**Order of cases:** `clean`, `bow_logon` (log on with ResetSeqNumFlag=Y), `midweek_logon` (the venue is ahead:
ResendRequest, accept the GapFill), `gap_resend`, `multiple_gaps`, `realtime_during_resend`, `venue_resend_request`
(answer with a GapFill), `venue_logout` (confirm, log on again with the right sequence, recover what you missed),
`heartbeat_lapse` (TestRequest after 2 × HeartBtInt, reconnect when it goes unanswered). The two reconnect cases run
75 s because a client's retry timer is typically 30 s.

**Drive it yourself — the `manual` case:** `withhold N` · `resend_request FROM [TO]` · `logout [text]` · `disconnect` ·
`lapse MS` (total silence) · `interleave on|off` · `test_request` · `seq_reset` · `mark LABEL` · `stats`.

---

## 3c · Nasdaq: feed handler (`nsdq-md-verifier`) and order gateway (`nsdq-oe-verifier`)

The same two procedures on the Nasdaq protocols, against one venue (`nsdq_me`: one order book behind both sides).

**Feed handler.** TotalView-ITCH 5.0 over MoldUDP64 on `233.54.12.121:26577` (iface `127.0.0.1`, session `MTSEQ0001`),
the retransmission server on `127.0.0.1:26578`, AAPL / MSFT / SPY (locate 1 / 2 / 3). No snapshot service: a late
joiner recovers the session from 1 by retransmission, as the venue keeps the day. The witness is the same shape as
CME's, keyed by the **MoldUDP64 sequence of the message just applied**, `secid` = locate, prices as decimals:
```
{"seq": 4812, "secid": 1, "symbol": "AAPL", "bid": [[189.99, 2300], [189.98, 500]], "ask": [[190.00, 800], [190.02, 500]]}
```
Cases: `clean` (late join), `drop_single`, `gap_burst`, `drop_sustained`, `duplicates`, `reorder`, `manual`
(`pause · resume · drop N · dup N · reorder N · rate MS · mark · stats` on `udp://127.0.0.1:26580`). BTS2's
`useq_witness` writes the witness from the UsEq shared-memory contract; start it before your handler so a late-join
recovery is not missed.

**Order gateway.** OUCH 4.2 over SoupBinTCP 3.0 on `tcp://<host>:27100` (any user / password; the login's requested
sequence number is honoured — the venue keeps the sequenced stream per user and replays from it). Session cases:
`clean`, `rejects`, `disconnect_replay` (the venue drops the TCP after 40 orders: log in again with your next
expected sequence and receive the replay), `heartbeat_lapse` (20 s of silence: drop after 15 s, log in again).
Order cases: `order_flow`, `partial_fills`, `cancel_fill_race` (OUCH sends Executed and **ignores** the cancel — no
Rejected — the order is filled), `unsolicited_cancels` (Canceled reason `S`); the order witness is the CME one keyed
by token (`examples/order_witness/nsdq_gw_witness.py` builds it from `nsdq_gw`'s log). The venue also counts cancels
for tokens it closed more than 2 s ago or never saw (`stale_cancels`): OUCH answers neither, so only the venue can tell
you. Control on `udp://127.0.0.1:26581`: `disconnect · lapse MS · reject N · partial N · fill_on_cancel N ·
unsolicited_cancel N · replace_reject N · mark · stats`.

---

## 3d · NYSE and NYSE Arca: feed handler (`nyse-md-verifier`, `arca-md-verifier`) and order gateway (`nyse-oe-verifier`, `arca-oe-verifier`)

One venue per exchange (`nyse_me` / `arca_me`, the same Pillar code), one order book behind both sides.

**Feed handler.** The XDP Integrated Feed on `233.54.14.121:26977` (NYSE; Arca `233.54.13.121:26787`), the recovery
group `.122` (retransmissions with delivery flag 10, refresh and mapping responses with 13) and the request server on
`tcp 127.0.0.1:26979` (Arca 26789): Retransmission Request 10, Refresh Request 15, Symbol Index Mapping Request 13. A
late joiner asks for the mapping and a refresh per symbol; nothing should be published for a symbol before its refresh
lands. The witness is the CME shape keyed by the **XDP stream sequence of the message just applied** (`secid` = the
symbol index from the mapping); a book rebuilt from a refresh goes under the refresh packet's sequence minus one.
Cases: `clean`, `drop_single`, `gap_burst`, `drop_sustained`, `duplicates`, `reorder`, `manual` (control
`udp://127.0.0.1:26980`, Arca 26790).

**Order gateway.** Pillar FIX (FIX 4.2) on `tcp://<host>:27210` (Arca 27110), TargetCompID `NYSE` / `ARCA`, any
SenderCompID. The venue keeps every outbound message per SenderCompID and serves ResendRequest with PossDupFlag=Y
replay and SequenceReset-GapFill; a Logon without ResetSeqNumFlag continues the stream, so `disconnect_resend` (the venue
drops the TCP; cancel-on-disconnect reports go on the stream) and `heartbeat_lapse` (20 s of silence) both end with
the client logging on again with its sequence continued, sending a ResendRequest and applying the replay. Plus
`rejects` and the order cases (`order_flow`, `partial_fills`, `cancel_fill_race` — the fill report first, then
OrderCancelReject "too late" — and `unsolicited_cancels`); `examples/order_witness/pillar_gw_witness.py` builds the
order witness from `nyse_gw` / `arca_gw`'s log. Control `udp://127.0.0.1:26981` (Arca 26791): `disconnect · lapse MS ·
reject N · partial N · fill_on_cancel N · unsolicited_cancel N · mark · stats`.

---

## 3e · Cboe BZX and CFE: feed handler (`bzx-md-verifier`, `cfe-md-verifier`) and order gateway (`bzx-oe-verifier`, `cfe-oe-verifier`)

One Cboe source built twice: `bzx_me` for equities (AAPL · MSFT · SPY, shares), `cfe_me` for the Cboe Futures Exchange
(VXZ6 · VXF7 on the 0.05 tick, contracts, with the PITCH Futures Instrument Definition and Settlement messages). One
order book behind both sides.

**Feed handler.** Multicast PITCH on `233.54.15.121:26877` (BZX; CFE `233.54.17.121:26887`), one matching unit,
Sequenced Unit Headers; the gap group `.122:26878` (CFE 26888) carries the retransmitted packets with their original
sequences, and the Gap Request Proxy / Spin server on `tcp 127.0.0.1:26879` (CFE 26889) answers Gap Request 0x03 and
Spin Request 0x81 (Spin Image Available every second; the spin's Add Orders arrive in sequence-0 headers, then Spin
Finished). The witness is the CME shape keyed by the **unit sequence of the PITCH message just applied** (the header's
sequence plus the message's index in the packet); PITCH carries no locate, so `secid` is your own handle and the report
joins on `symbol`. Cases: `clean`, `drop_single`, `gap_burst`, `drop_sustained`, `duplicates`, `reorder`, `manual`
(control `udp://127.0.0.1:26880`, CFE 26890).

**Order gateway.** BOE v2 on `tcp://<host>:27310` (CFE 27410), session sub id `0001` (CFE `0003`), any username. The
venue keeps every sequenced message per session and unit; a Login Request's Unit Sequences group names the last
received sequence and the venue replays from there and sends Replay Complete, so `disconnect_replay` (the venue drops
the TCP; the cancels it made go on the stream) and `heartbeat_lapse` (silence, no Server Heartbeats) both end with
the client logging in again, naming its sequence and applying the replay — the first run found the reference gateway
keeping alive a New Order the venue never received (the Login Response's LastReceivedSequenceNumber said so), fixed.
Plus `rejects` and the order cases (`order_flow`, `partial_fills`, `cancel_fill_race` — Order Execution first, then
Cancel Rejected — and `unsolicited_cancels`, Order Cancelled reason A); `examples/order_witness/boe_gw_witness.py`
builds the order witness from `bzx_gw` / `cfe_gw`'s log. Control `udp://127.0.0.1:26881` (CFE 26891): `disconnect ·
lapse MS · reject N · partial N · fill_on_cancel N · unsolicited_cancel N · mark · stats`.

---

## 3f · Coinbase Exchange: feed handler (`coinbase-md-verifier`) and order gateway (`coinbase-oe-verifier`)

The first crypto venue: one simulator, `coinbase_me`, written from Coinbase's published WebSocket-feed and FIX 4.2
order-entry documentation, serving both sides of the venue behind one order book (BTC-USD, ETH-USD; a random walk in
basis points, synthetic takers; clients' orders rest in the level2 book at their price). The reference clients are the
CTS crypto system's `coinbase_md` and `coinbase_gw` (the ones behind the public crypto page).

**Feed handler.** `ws://127.0.0.1:27600/` — plain ws for the bench (set your handler's TLS off): `subscribe` with
`level2_batch`, `matches`, `ticker`, `heartbeat` → `subscriptions`, a full `snapshot` per product, then `l2update`
batches every 100 ms with a `time`, `match` / `last_match` and `ticker` with the product's sequence, a `heartbeat` a
second. There is no sequence on level2 and no retransmission: a lost or dead connection means reconnect, resubscribe
and a fresh snapshot. The witness is the CME shape keyed by the **l2update's `time` as UTC ns** (one line per l2update
after all its changes; `secid` is your handle, the report joins on `symbol` + time). Cases: `clean`, `disconnect` (the
venue closes the socket at 20 s — right again within 5 s), `silence` (a dead socket for 40 s: no frames, not even
heartbeats — detect it, reconnect, right again within 45 s), `burst` (one l2update of 400 changes), `churn` (20 ms
batches), `manual` (control `udp://127.0.0.1:27602`: `pause · resume · ws_disconnect · ws_silence MS · burst N · rate MS ·
mark · stats`). Reference witness: `cts_witness --ring coinbase_md ...` (rebuilds the book from the handler's SHM ring
exactly as a strategy would; rewinds to the ring's oldest event so the snapshot is seen).

**Order gateway.** FIX 4.2 over TLS on `tcp://<host>:27601` (a self-signed certificate; the reference client does not
verify the peer): Logon with 141=Y, a 96 signature (accepted as given) and 8013=Y; NewOrderSingle with a UUID ClOrdID,
OrderCancelRequest, ExecutionReports 150=0/1/2/4/8, OrderCancelReject, TestRequest; a ResendRequest gets a session
Reject — Coinbase has no resend. Cancel-on-disconnect is honoured the way the venue does it: a dropped session's resting
orders are cancelled and nobody is told, so `disconnect` and `heartbeat_lapse` both require the client to close its
working orders itself on reconnect (the truth carries those cancels as `cod_cancel`). Plus `test_request`, `rejects`
and the order cases (`order_flow`, `partial_fills`, `cancel_fill_race` — the fill first, then 35=9 "Order already
done" — and `unsolicited_cancels`); `examples/order_witness/coinbase_gw_witness.py` builds the order witness from
`coinbase_gw`'s log. Control `udp://127.0.0.1:27603`: `disconnect · silence MS · test_request · reject N · partial N ·
fill_on_cancel N · unsolicited_cancel N · mark · stats`. The first run found three things in the reference gateway — a
dead session going unnoticed (SO_RCVTIMEO never fires under asio's blocking read), a `bad record mac` from concurrent
SSL_read / SSL_write on one SSL object, and acknowledgements arriving ~100 ms late (TLS records parked in asio's memory
BIO, invisible to poll) — all fixed; none had shown on the live venue at 30 s heartbeats and one order a second.

---

## 3g · Kraken: feed handler (`kraken-md-verifier`) and order gateway (`kraken-oe-verifier`)

One simulator, `kraken_me`, written from Kraken's published WebSocket v2 and REST documentation: the v2 server (public
channels and the private `executions` channel on one socket, `ws://127.0.0.1:27700/v2`) and HTTPS REST on `27701`
(a self-signed certificate: Time, GetWebSocketsToken, AddOrder, CancelOrder, QueryOrders; API-Key / API-Sign accepted
as given), one order book per pair (BTC/USD tick 0.1, ETH/USD tick 0.01). The reference clients are CTS's `kraken_md`
and `kraken_gw`.

**Feed handler.** Subscribe `book` (depth 10 on the bench), `ticker`, `trade`, `heartbeat`: an ack per symbol, a
snapshot with its checksum, then updates with checksum and timestamp. Two things this venue does that others do not:
the **depth-window rule** (a level pushed out of the window by a better one gets no delete — truncate after every
update; a level removed inside it gets qty 0) and the **checksum** (CRC32 over the top 10 asks then bids, each level's
price and quantity formatted at the pair's precision with the point and leading zeros removed — the only integrity
signal, since v2 carries no sequence and there is no retransmission: a mismatch means resubscribe). Witness: the CME
shape keyed by the **update's `timestamp` as UTC ns**, one line per update after applying and truncating. Cases:
`clean`, `disconnect`, `silence`, `lost_update` (one update per pair is not sent, carrying a jump of the touch — the
handler must notice the next checksum and resubscribe; right again within 5 s), `burst`, `churn`, `manual` (control
`udp://127.0.0.1:27702`: `pause · resume · ws_disconnect · ws_silence MS · lost_update · burst N · rate MS · mark ·
stats`). The first run found the reference handler logging the checksum and never checking it: a lost update left a
stale touch for the rest of the run. Fixed (validate every update, resubscribe the symbol on a mismatch).

**Order gateway.** Signed REST `AddOrder` (ordertype, type, volume, pair, price, oflags=post, timeinforce=IOC,
cl_ord_id) → `txid`; `CancelOrder` by txid → `count`, or `EOrder:Unknown order`; `QueryOrders` for an order's fate; a
`GetWebSocketsToken` token opens the `executions` channel, whose snapshot lists the open orders (with `cum_qty`) and
whose updates carry `new` / `trade` / `canceled` with `cl_ord_id`. Cases: `clean`, `rejects`, `exec_disconnect` (the
executions socket closed after 30 orders while REST keeps working: fills happen in the gap; mint a token, resubscribe,
reconcile every working order from the snapshot and QueryOrders those no longer open), `rest_down` (503
`EService:Unavailable` for 8 s: surface the refusals, resume), `order_flow`, `partial_fills`, `cancel_fill_race` (the
trade on the channel, then `CancelOrder` answers `EOrder:Unknown order`), `unsolicited_cancels`, `manual` (control
`udp://127.0.0.1:27703`). `examples/order_witness/kraken_gw_witness.py` builds the order witness from `kraken_gw`'s
log. The first run found two things in the reference gateway: it skipped the executions snapshot (anything executed
while the socket was down was never booked — it now reconciles from the snapshot's `cum_qty` and asks QueryOrders
for orders no longer open), and every REST call took 40–80 ms (httplib writes headers and body separately; Nagle met
delayed ACK — `TCP_NODELAY`, 1 ms now); the stale quotes that latency produced had shown as post-only refusals.

---

## 3h · Binance: feed handler (`binance-md-verifier`) and order gateway (`binance-oe-verifier`)

One simulator, `binance_me`, written from Binance's published WebSocket-streams, WebSocket-API, user-data-stream and
REST documentation (the spot docs as of 2026): the combined stream (`/stream?streams=btcusdt@depth@100ms/...`) and the
WebSocket API (`/ws-api/v3`) on one plain-ws port (`27800`), HTTPS REST on `27801` (a self-signed certificate: depth,
order, openOrders, ping, time; the API key and signature accepted as given), one order book per symbol (BTCUSDT,
ETHUSDT, tick 0.01) where every level change gets an update id; a ping frame every 20 s. The reference clients are
CTS's `binance_md` and `binance_gw`. (Until 0.9 this pair was the Binance.US edition, with the listenKey user stream;
Binance deprecated that stream 2025-04-07 and removed it from the spot documentation 2025-10-24.)

**Feed handler.** The recipe is the contract: a REST `/depth` snapshot with `lastUpdateId` (up to 5000 levels),
`depthUpdate` diffs carrying `U..u`; drop events with `u ≤ lastUpdateId`, the first applied one must straddle the
snapshot, then `U` must be `last+1`; a gap means a fresh snapshot, and a snapshot older than the buffered events must be
fetched again. Witness: the CME shape keyed by the **update's final update id `u`**. Cases: `clean`, `drop_depth` (one
diff per symbol not sent → the gap, re-snapshot, right again within 5 s), `stale_snapshot` (`/depth` answers a cached
snapshot ~4 s behind the stream for 6 s — fetch again until it is not), `disconnect`, `silence`, `burst`, `manual`
(control `udp://127.0.0.1:27802`). The first run found the reference handler dying with SIGPIPE at its first resync
(the venue had closed the idle REST keep-alive socket; no CTS process ignored the signal — fixed in all of them) and,
after re-snapshotting, applying the triggering event without re-checking continuity, so a stale snapshot got deltas on
top.

**Order gateway.** `order.place` / `order.cancel` on the WebSocket API (or signed REST); the user data stream is a
subscription on the WebSocket API connection — `userDataStream.subscribe` after an Ed25519 `session.logon`, or
`userDataStream.subscribe.signature` (apiKey + timestamp + signature, any key type) — answered with a `subscriptionId`,
after which `executionReport`s (x NEW / TRADE / CANCELED / EXPIRED, z the cumulative quantity) arrive on that connection
as `{"subscriptionId", "event"}`; `eventStreamTerminated` ends a subscription. Binance replays nothing: what executed
while the subscription was down is found only by asking (`GET /api/v3/order` per working order). Cases: `clean`,
`rejects` (-2010), `stream_disconnect` (the connection holding the subscription is closed; order entry keeps working
over REST), `stream_terminate` (the venue ends the subscription, the connection stays), `wsapi_disconnect` (every WS
API socket closed — the subscription dies with it), `rate_limit` (429 for 6 s), `order_flow`, `partial_fills`,
`cancel_fill_race` (the TRADE, then -2011), `unsolicited_cancels` (x EXPIRED), `manual` (control
`udp://127.0.0.1:27803`). `examples/order_witness/binance_gw_witness.py` builds the order witness from `binance_gw`'s
log. The first runs found the reference gateway never reconciling after the stream came back, reconnecting with a
stale credential, and letting a cancel race the `order.place` response — all fixed (reconcile on every subscription, a
fresh subscription per connect and after a termination, cancels parked until the acknowledgement).

## 3j · OKX: feed handler (`okx-md-verifier`) and order gateway (`okx-oe-verifier`)

One simulator, `okx_me`, written from OKX's published v5 WebSocket and REST documentation: `/ws/v5/public` and
`/ws/v5/private` on one plain-ws port (`28000`), HTTPS REST on `28001` (a self-signed certificate: `/api/v5/trade/order`,
`cancel-order`, `order`, `orders-pending`, `/api/v5/public/time`; the OK-ACCESS-* headers accepted as given), one order
book per instrument (BTC-USDT tick 0.1, ETH-USDT tick 0.01; levels as `[px, sz, "0", n]` strings). The reference
clients are CTS's `okx_md` and `okx_gw`.

**Feed handler.** The `books` channel: a snapshot on subscribe (`prevSeqId` -1, 400 levels) then updates of the changed
levels chained by `prevSeqId` = the last `seqId` — the checksum is fixed to 0 since 2026-06-23, so the chain is the only
continuity proof; a mismatch means resubscribe for a fresh snapshot; after ~60 s without a change an update with empty
sides and `seqId == prevSeqId` says the connection is alive; a maintenance reset is an update whose `seqId` is smaller
than its `prevSeqId` and is valid — the chain continues from it. `bbo-tbt`, `trades`, the literal `ping` / `pong`, the
30 s rule (no subscription and nothing pushed → closed). Witness: the CME shape keyed by the **update's `seqId`**, at
depth 20. Cases: `clean`, `disconnect`, `silence`, `lost_update` (one update per instrument withheld — see the gap,
resubscribe, right again within 5 s), `seq_reset` (apply it, it is not a gap), `quiet_market` (65 s: the heartbeat,
the 30 s rule, one connection), `churn` (20 ms), `manual` (control `udp://127.0.0.1:28002`). The first run found the
reference handler applying every update blindly: a lost update left a stale touch for the rest of the run. It now
chains `prevSeqId`, resubscribes on a gap, skips the heartbeat and applies a reset.

**Order gateway.** Signed REST order entry (`POST /api/v5/trade/order`, `cancel-order`, the `sCode` envelope), the
`orders` channel on the private WebSocket after `login` — it pushes **nothing on subscribe**, and a cancel's `sCode 0`
means the request was accepted, not that the order is cancelled (OKX's own note: the result is subject to the channel
or a query). Cases: `clean`, `rejects` (51127), `exec_disconnect` (the private socket closed after 30 orders; fills in
the gap; reconnect, log in, subscribe, reconcile with `orders-pending` + `order`), `rate_limit` (429 / 50011 for 6 s),
`order_flow`, `partial_fills`, `cancel_fill_race` (accepted, then filled — never canceled), `unsolicited_cancels`
(cancelSource 0), `manual` (control `udp://127.0.0.1:28003`). `examples/order_witness/okx_gw_witness.py` builds the
order witness from `okx_gw`'s log. The first run found the reference gateway publishing CANCELED on the REST accept and
never reconciling after the private socket came back — both fixed.

## 3i · Hyperliquid: feed handler (`hyperliquid-md-verifier`) and order gateway (`hyperliquid-oe-verifier`)

One simulator, `hyperliquid_me`, written from Hyperliquid's published API documentation (the WebSocket, the exchange and
info endpoints, nonces, rate limits, error responses) and the official SDK's signing scheme: one plain-ws endpoint
(`ws://127.0.0.1:27900/ws`) with `subscribe` / `unsubscribe` / `ping` / `post`, the `l2Book`, `trades`, `bbo` and
`activeAssetCtx` channels, the `orderUpdates` and `userFills` user subscriptions, the `error` channel, the 60 s idle rule;
HTTPS on `27901` (a self-signed certificate) with `POST /exchange` and `POST /info`. One order book per coin (BTC tick
1, ETH tick 0.1; prices and sizes as strings, 20 levels per side; minimum order value $10). The reference clients are
CTS's `hyperliquid_md` and `hyperliquid_gw`.

**Feed handler.** Every `l2Book` message is the whole book — no sequence, no diffs, no checksum — so the snapshot
replaces the book, never merges into it; `bbo` carries `null` for a side with nothing resting. Witness: the CME shape
keyed by the **snapshot's `time` (ms × 1 000 000)**, one line per snapshot including the first after a subscribe, at
depth 20. Cases: `clean`, `disconnect`, `silence` (no heartbeat channel exists — the handler's own read timeout has to
notice), `quiet_market` (the market stops for 75 s: nothing is sent; a handler that pings only on the back of inbound
frames is torn down — by its own watchdog or by the venue's idle rule — and must instead stay on one connection),
`thin_book` (the ask side thins to 3 levels, then to none), `churn` (20 ms), `manual` (control `udp://127.0.0.1:27902`:
`pause · resume · ws_disconnect · ws_silence MS · quiet MS · thin MS · rate MS · mark · stats`). The first run found the
reference handler pinging only when a frame arrived: through the quiet market its 30 s read watchdog reconnected it
twice for nothing (each fresh connection restarting the venue's idle clock — with a longer watchdog the venue would
have closed it instead). It now pings every 20 s on a timer; the pong feeds the watchdog.

**Order gateway.** Hyperliquid has no session and no API key: every `/exchange` request is a signed L1 action and the
venue identifies the account by **recovering the signer** — `keccak256(msgpack(action) ‖ nonce ‖ 0x00)` under an EIP-712
`Agent` (domain `Exchange` / `1` / chainId 1337), secp256k1 with RFC 6979. The bench venue recovers signers with the
same code, so a wrong byte anywhere in what was signed answers the docs' `L1 error: User or API Wallet 0x… does not
exist.` (the CEX benches accept signatures as given; this one cannot). The nonce rules apply (strictly larger than the
smallest of the 100 highest per signer, never reused, inside the window). Orders `{a, b, p, s, r, t: {limit: {tif Gtc |
Alo | Ioc}}, c: cloid}` answer `{resting: {oid}}`, `{filled: {totalSz, avgPx, oid}}` or `{error}` — a post-only that
would cross is rejected at placement, never accepted-then-cancelled; `cancelByCloid` answers `"success"` or `Order was
never placed, already canceled, or filled.`. Order state is `orderUpdates` (the remaining `sz`, the status vocabulary:
open, filled, canceled, marginCanceled, …) and `userFills` (per fill with `tid`; the first message a snapshot) on the
WebSocket; nothing is replayed, so reconcile with `POST /info orderStatus` by cloid. Cases: `clean` (with
`signatures_valid`), `rejects` (scripted `Insufficient margin` plus the natural post-only rejections), `user_disconnect`
(fills in the gap; reconcile), `address_limit` (the docs' `Too many cumulative requests sent …` error for 6 s — the limit
that bites is per address, 1 request per 1 USDC traded), `order_flow`, `partial_fills`, `cancel_fill_race`,
`unsolicited_cancels` (marginCanceled), `manual` (control `udp://127.0.0.1:27903`). `examples/order_witness/
hyperliquid_gw_witness.py` builds the order witness from `hyperliquid_gw`'s log (the cloid carries the cl_ord_id). The
reference gateway was built for this tool: until 0.10 it was a scaffold that shelled out to a Python signer per order
and had no fill path; it now signs natively — byte-identical to the official SDK, `tests/hl_sign_equivalence.py` in CTS is
the proof — and books fills from both channels against a watermark.

## 3k · dYdX v4: feed handler (`dydx-md-verifier`) and order gateway (`dydx-oe-verifier`)

One simulator, `dydx_me`, written from the indexer's own source (dydxprotocol/v4-chain, `indexer/services/socks`: the
`connected` / `subscribed` / `channel_data` / `channel_batch_data` / `unsubscribed` / `error` messages and their exact
texts, `message_id` bumped on every message sent, the 2-per-second subscribe limit per channel + id and its 1008 close,
the 30 s ping / 10 s pong heartbeat), the docs' orderbook pages (absolute sizes, the crossing caveat, the newest-wins
rule), the chain's protos and clob-module source (the codes and texts, ShortBlockWindow 40, the sequence rule for
short-term orders), the Cosmos SDK REST surface and the indexer's own types. One plain-ws endpoint
(`ws://127.0.0.1:28100/v4/ws`), node + indexer HTTPS on `28101`; BTC-USD (tick 1) and ETH-USD (tick 0.1). The
reference clients are CTS's `dydx_md` and `dydx_gw`.

**Feed handler.** The indexer has no sequence on the book but `message_id`, a per-connection counter that every
message the server sends takes — a gap is a lost frame and the only recovery is a resubscribe for a fresh snapshot.
And the docs' caveat: nothing guarantees the two sides do not cross ("the correct orderbook at any given time is
whatever the current block proposer has in its mempool"); the rule is that the newer update wins. Witness: the CME
shape keyed by the **message's `message_id`** (it restarts at 0 on a new connection — the report joins per
connection), at depth 20. Cases: `clean`, `disconnect`, `silence`, `drop_message` (one channel_data per market
withheld, its message_id consumed — see the gap, resubscribe), `cross` (for 8 s the levels that took the touch come
now and the deletions of the levels they crossed one message later: 88 crossed updates in the reference run, 0 wrong
books for a handler that uncrosses, 88 for one that does not), `burst`, `churn`, `manual` (control
`udp://127.0.0.1:28102`). The first run found the reference handler never checking `message_id`: a lost frame left a
stale level in its book for the rest of the run and nothing noticed. It now expects every message_id in sequence and
resubscribes on a gap.

**Order gateway.** Order entry is a Cosmos SDK transaction: `MsgPlaceOrder` / `MsgCancelOrder` in a `TxBody`, an
`AuthInfo` with the secp256k1 pubkey and SIGN_MODE_DIRECT, a signature over `sha256(SignDoc)`, broadcast as base64
`TxRaw` to `POST /cosmos/tx/v1beta1/txs`. The bench venue decodes the transaction, **verifies the signature** against
the pubkey in its AuthInfo and requires the derived address (`bech32 dydx`) to own the order — a wrong byte anywhere
in what was signed is "signature verification failed". Short-term orders (order_flags 0) expire at their `goodTilBlock`,
which must be within 40 blocks of the height (clob 10 / 11), skip the sequence check, cost no gas; a post-only that
would cross is refused in CheckTx (clob 2003); a cancel of a filled order is clob 24. Units are quantums / subticks
from `/v4/perpetualMarkets`. Order state is the indexer's `v4_subaccounts` channel ({orders} with status and
totalFilled, {fills}) and `v4_block_height`; nothing is replayed — reconcile with `GET /v4/orders`. Cases: `clean`
(with `signatures_valid`), `rejects` (subaccounts 102 plus the natural post-only refusals), `user_disconnect`,
`node_down` (503 for 6 s), `expiry` (300 ms blocks and a 3-block goodTilBlock: quotes that rest are removed EXPIRED —
book them cancelled), `order_flow`, `partial_fills`, `cancel_fill_race` (clob 24), `unsolicited_cancels`
(UNDERCOLLATERALIZED), `manual` (control `udp://127.0.0.1:28103`). `examples/order_witness/dydx_gw_witness.py` builds
the order witness from `dydx_gw`'s log (the venue knows the clientId, the low 32 bits of the cl_ord_id). The reference
gateway was built for this tool: until 0.12 it was a scaffold that shelled out to a Python signer per order and had no
fill path; it now builds and signs the transaction natively — byte-identical to the official dydx-v4-client,
`tests/dydx_tx_equivalence.py` in CTS is the proof — and books fills from the indexer against a watermark.

## 3l · Injective: feed handler (`injective-md-verifier`) and order gateway (`injective-oe-verifier`)

One simulator, `injective_me`, written from the chain's public interfaces as a feed handler meets them: the Tendermint
RPC WebSocket (JSON-RPC 2.0 `subscribe` with `tm.event='NewBlock'`, the event envelope, 5 subscriptions per client), the
gRPC-gateway LCD (`/injective/exchange/v1beta1/{derivative|spot}/orderbook/{market_id}` as chain Dec strings, with the
`Grpc-Metadata-X-Cosmos-Block-Height` header every gRPC-gateway answer carries) and the indexer trades listing (every
fill twice: the taker and the maker record). Tendermint ws on `28200`, LCD + indexer HTTPS on `28201`; a block every
700 ms; BTC/USDC PERP and ETH/USDC PERP. For the gateway it also plays the node (CheckTx, DeliverTx at the block, the
EndBlocker, the tx query, the v2 markets and orders queries) and the Chain Stream WebSocket server on `28203`, from the
chain's published protos, the exchange module's error registry, its module docs and the node's websocket-server doc.
The reference clients are CTS's `injective_md` and `injective_gw`.

**Feed handler.** The book is chain state and changes only at block boundaries (matching is a batch auction at the end
of every block), so a height is the whole story: one LCD book per block, keyed by the height the node *served* — the
header, not the announcement. Witness: the CME shape keyed by that **height**, in human units, at depth 20. Cases:
`clean`, `disconnect` (the block stream closes; the chain goes on), `stream_silence` (NewBlock stops for 20 s while
blocks keep coming: poll on the timer, key by the served height — a handler that waits for the announcement freezes),
`lcd_lag` (the node is one block behind for 8 s: its header says so), `lcd_down` (503 for 6 s), `halt` (no blocks for
12 s: no phantom heights), `churn` (200 ms blocks), `manual` (control `udp://127.0.0.1:28202`). The first run found two
things in the reference handler: it keyed the book by the announced height and never read the LCD's height header, so a
lagging node gave a stale book a fresh height; and when the block stream died its timer fallback polled but never
published (the announced height had not changed, so the poll was skipped) — the book froze. It now keys by the served
height, polls again behind a lagging node and publishes on the timer.

---

**Order gateway.** Order entry is a Cosmos SDK transaction — `MsgCreateDerivativeLimitOrder` / `MsgCancelDerivativeOrder`
(`injective.exchange.v2`, human-readable units × 10^18 on the wire) — signed with an **Ethereum-style key**: the pubkey
travels as `/injective.crypto.v1beta1.ethsecp256k1.PubKey`, the signature is secp256k1 over `keccak256(SignDoc)`, the
address is bech32 `inj` of the Ethereum address, the subaccount that address + a 12-byte index. Every transaction costs
gas (fee ≥ gas × 160000000 inj) and carries the account's sequence, which must be the *mempool's* next — committed plus
the transactions already accepted this block; the wrong one is `account sequence mismatch, expected N` (sdk 32). The
bench venue decodes the transaction, verifies the signature, requires the signer to own the subaccount, checks the
sequence and the fee — and that is all a broadcast answers: **the message runs at the next block**, and its outcome
(exchange 6 `subaccount has insufficient deposits`, 19 `order doesnt exist`, 97 `client order id already exists`, …)
is the tx result, `GET /cosmos/tx/v1beta1/txs/{hash}` (404 code 5 until the block). A post-only that would cross is
failed by the EndBlocker (exchange 59), and the only notice is an `order_failures` entry on the chain stream. Order state
is the node's Chain Stream WebSocket server (`ws://…/injstream-ws`, JSON-RPC 2.0 subscribe with
`derivative_orders_filter` + `derivative_trades_filter` on the subaccount and `order_failures_filter` on the address):
per block `derivative_orders` (Booked | Matched | Cancelled with the order's `fillable`), `derivative_trades` (by cid),
`order_failures`, `block_height`; nothing is replayed — reconcile with `GET /injective/exchange/v2/derivative/{orders|
transient_orders}/{market}/{subaccount}` and the indexer's trades for the subaccount. Cases: `clean` (with
`signatures_valid` and `tx_results_checked`), `rejects` (deliver-time 6 plus the natural post-only 59), `stream_disconnect`,
`node_down` (503 for 6 s), `seq_mismatch` (the sequence jumps by 3: resync from the refusal text, keep sending), `expiry`
(300 ms blocks and a 3-block `expiration_block`: quotes that rest are cancelled by the chain — book them cancelled),
`order_flow`, `partial_fills`, `cancel_fill_race` (exchange 19 in the tx result), `unsolicited_cancels`, `manual`
(control `udp://127.0.0.1:28204`). `examples/order_witness/injective_gw_witness.py` builds the order witness from
`injective_gw`'s log (the venue knows the cid, `cts-<cl_ord_id>`). The reference gateway was built for this tool: until
0.15 it was a scaffold that shelled out to a Python signer per order and had no fill path; it now builds and signs the
transaction natively — byte-identical to the official injective-py, RFC 6979 on HMAC-keccak256 included,
`tests/inj_tx_equivalence.py` in CTS is the proof — follows every transaction up with the tx query, and books fills from
the chain stream against a watermark.

## 3m · IEX: feed handler (`iex-md-verifier`) and order gateway (`iex-oe-verifier`)

IEX's own protocols, built from the exchange's current documents — IEX-TP v1.25, DEEP v1.08, DEEP SNAP v1.6,
IEX FIX v3.11 — against one venue (`iex_me`: one price-level book behind both sides).

**Feed handler.** DEEP v1.08 over IEX-TP v1 on `233.54.26.121:25487` (iface `127.0.0.1`; Message Protocol ID
`0x8004`, Channel 1, a fresh Session ID per venue start), the IEX-TP Gap Fill Server on `udp://127.0.0.1:25489`
(Sequenced Messages requests, at most 1,000 messages each) and DEEP SNAP on `tcp://127.0.0.1:25489` (any non-empty
token). Every case starts as a late joiner: the book comes from DEEP SNAP, holes are closed by Gap Fill Requests. The
witness is the CME shape keyed by the **IEX-TP sequence of the Price Level Update that completed the event** (Event
Flags = 1), `secid` = locate (the position in the symbol list: AAPL 1, MSFT 2, SPY 3), prices as decimals:
```
{"seq": 4812, "secid": 1, "symbol": "AAPL", "bid": [[189.99, 2300], [189.98, 500]], "ask": [[190.00, 800], [190.02, 500]]}
```
Cases: `clean` (the late join), `drop_single`, `gap_burst`, `drop_sustained`, `duplicates`, `reorder`, `manual`
(`pause · resume · drop N · dup N · reorder N · rate MS · mark · stats` on `udp://127.0.0.1:25490`).

**Order gateway.** IEX FIX v3.11 (FIX 4.2) on `tcp://<host>:28510`: Logon with any SenderCompID, TargetCompID `IEXG`,
108 required, 141 honoured; `D` / `F` / `G` in, `8` / `9` out; the venue keeps a replayable stream per SenderCompID and
serves Resend Requests with PossDup and Sequence Reset-GapFill. No cancel-on-disconnect in the specification (a port
attribute), so a dropped session keeps its resting orders. Session cases: `clean`, `rejects` (150=8 / 103=0),
`disconnect_replay` (drop after 40 orders with the last reports on the stream: log on again continuing your
sequence, Resend Request the gap), `heartbeat_lapse` (20 s of silence: Test Request, drop after 2.5 × HeartBtInt —
5 s on the bench — log on again, Resend Request). Order cases: `order_flow`, `partial_fills` (150=1),
`cancel_fill_race` (the fill first, then Order Cancel Reject 102=0: filled, not cancelled), `unsolicited_cancels`
(150=4 with 11 = 41, text AdminCancel); the order witness is the CME one keyed by ClOrdID
(`examples/order_witness/iex_gw_witness.py`). Control on `udp://127.0.0.1:25491`: `disconnect · lapse MS · reject N ·
partial N · fill_on_cancel N · unsolicited_cancel N · replace_reject N · mark · stats`.

---

## 3n · MEMX: feed handler (`memx-md-verifier`) and order gateway (`memx-oe-verifier`)

MEMX's own protocols — MEMOIR Depth v2.0 over MEMX-UDP v1.1, MEMO SBE v2.0 over MEMX-TCP v1.2, with the June 2026
errata — against one venue (`memx_me`: one order book behind both sides). Provenance: the documents are the ones MEMX
publishes through LTSE (which runs on MEMX technology, "used under license"); MEMX's own editions sit behind its member
portal, so venue-configured values (session id 1, user MTS01, MPID MTSM, 1 s heartbeats / 15 s timeout, a 1,000-message
replay cap, the penny grid) are bench choices.

**Feed handler.** MEMX-UDP on `233.54.27.121:25587` (iface `127.0.0.1`, session 1), the MEMX-TCP gap-fill server in
Replay mode on `tcp://127.0.0.1:25589` (login `user:password`, Replay Request → Replay Begin / Sequenced Messages /
Replay Complete, 1,000 per request), AAPL / MSFT / SPY (SecurityID 1 / 2 / 3). No snapshot service in this phase: a
late joiner replays the session from 1. The witness is the CME shape keyed by the **MEMX-UDP sequence of the message
just applied**, `secid` = SecurityID, prices as decimals (mantissa / 1e6):
```
{"seq": 4812, "secid": 1, "symbol": "AAPL", "bid": [[189.99, 2300], [189.98, 500]], "ask": [[190.00, 800], [190.02, 500]]}
```
Cases: `clean` (late join), `drop_single`, `gap_burst`, `drop_sustained`, `duplicates`, `reorder`, `manual`
(`pause · resume · drop N · dup N · reorder N · rate MS · mark · stats` on `udp://127.0.0.1:25590`).

**Order gateway.** MEMO SBE over MEMX-TCP in Stream mode on `tcp://<host>:28610` (Login Request token `P`
`user:password`, any; Start of Session; a Stream Request from your next expected sequence is honoured and replayed).
A cancel that finds nothing is answered with OrderCancelReject (TooLateToCancel in the in-flight window, otherwise
UnknownOrigOrder); cancel-on-disconnect is on (CancelReason 13, the reports replay on re-login). Session cases:
`clean`, `rejects` (ExecutionReport_Rejected), `disconnect_replay` (drop after 40 orders: log in again, Stream Request
from your next sequence), `heartbeat_lapse` (20 s of silence: drop after 15 s, log in again). Order cases:
`order_flow`, `partial_fills`, `cancel_fill_race` (ExecutionReport_Trade then OrderCancelReject TooLateToCancel:
filled), `unsolicited_cancels` (ExecutionReport_Canceled, CancelReason 7); the order witness is the CME one keyed by
ClOrdID (`examples/order_witness/memx_gw_witness.py`). Control on `udp://127.0.0.1:25591`: `disconnect · lapse MS ·
reject N · partial N · fill_on_cancel N · unsolicited_cancel N · replace_reject N · mark · stats`.

---

## 3o · MIAX Pearl Equities: feed handler (`miax-md-verifier`) and order gateway (`miax-oe-verifier`)

MIAX's own protocols, built from the exchange's current documents — Depth of Market feed 1.3.d over MACH 1.2e,
MEO 2.7.b over ESesM 1.0.a, the Port Attributes and Liquidity Indicator Codes — against one venue (`miax_me`: one
order book behind both sides).

**Feed handler.** MACH on `233.54.28.121:25387` (iface `127.0.0.1`, session 1; one DoM message per MACH packet),
the retransmission interface over ESesM on `tcp://127.0.0.1:25389` (login with requested sequence 0, then a gap-fill
range or the Order Book Refresh; one request per connection — the interface sends the data, GoodBye, then closes),
AAPL / MSFT / SPY (Symbol ID 1 / 2 / 3). Every case starts as a late joiner rebuilt from the Order Book Refresh. The
witness is the CME shape keyed by the **MACH sequence of the DoM message just applied**, `secid` = Symbol ID, prices
as decimals (Prc6U divided out):
```
{"seq": 4812, "secid": 1, "symbol": "AAPL", "bid": [[189.99, 2300], [189.98, 500]], "ask": [[190.00, 800], [190.02, 500]]}
```
Cases: `clean` (late join), `drop_single`, `gap_burst`, `drop_sustained`, `duplicates`, `reorder`, `manual`
(`pause · resume · drop N · dup N · reorder N · rate MS · mark · stats` on `udp://127.0.0.1:25390`).

**Order gateway.** MEO 2.7.b over ESesM 1.0.a on `tcp://<host>:28710` (login `l`: any username / computer ID, one
matching engine; the requested sequence per engine is honoured — replay then Synchronization Complete; responses are
sequenced only when successful). The bench session elects Auto Cancel on Disconnect = Cancel All Open Orders (port
attribute 7). Session cases: `clean`, `rejects` (New Order Response with a reject status), `disconnect_replay` (drop
after 40 orders, open orders cancelled by ACOD: log in again with your next expected sequence, take the replay),
`heartbeat_lapse` (20 s of silence: drop after three heartbeat intervals, log in again). Order cases: `order_flow`,
`partial_fills`, `cancel_fill_race` (Execution Notification then Cancel Order Response `D`: filled),
`unsolicited_cancels` (Cancel/Reduce Size Notification, reason `H`); the order witness is the CME one keyed by the
client order id (`examples/order_witness/miax_gw_witness.py`). Control on `udp://127.0.0.1:25391`: `disconnect ·
lapse MS · reject N · partial N · fill_on_cancel N · unsolicited_cancel N · replace_reject N · mark · stats`.

---

## 3p · FINRA TRACE: the tape (`trace-tape-verifier`)

TRACE is not a venue: every FINRA member reports each corporate-bond trade and FINRA disseminates the reports on the
Bond Trade Dissemination Service. The tool is built from FINRA's **public** BTDS 2.1 (MOLD/UDP 64) specification
(effective 2024-10-21) carried by Nasdaq's MoldUDP64 1.00 — both public downloads, so this bench is in the catalogue
where the credit venues' dealer lanes (MarketAxess, Tradeweb, Trumid) are not.

The simulator `trace_ds` (BTS2) disseminates the tape byte for byte: the 24-byte header (category, type, 7-digit trade
id, market center, Eastern date-time), the 123-byte Trade Report / 206-byte Cancel / 280-byte Correction, the 116-byte
Daily Trade Summary and 89-byte Trading Halt, the six header-only control messages; quantities right-justified with
the decimal in the twelfth position or `5MM+` / `1MM+` (Quantity Indicator E) above the caps — investment grade $5MM,
high yield $1MM; prices `$$$$.dddddd`, yields `$$$$$$.dddddd` with a direction byte; the issue's high / low / last kept
by the sale-condition matrix (Appendix C: current-day, not special-price, blank or Z conditions update; T / U / W / P
do not; last only when the execution is not earlier than the previous) and the Change Indicator set from it; a cancel
and a correction carry the adjusted Summary. All of it as MoldUDP64 message blocks: session `BTDS000001`, a heartbeat
every second when idle, end-of-session packets after End of Transmissions, a unicast retransmission server that serves
the session's history. The faults are the MoldUDP64 ones (drop, burst, duplicate, reorder) plus the tape's: a quiet
spell after lost prints (only the heartbeat's sequence tells), cancels, corrections, as-of trades and reversals,
special prices, late / after-hours / weighted-average prints, a halt and its lift, the close of the day (C/C, a Daily
Trade Summary per issue that traded, C/X, C/J, C/Z), a reader that starts mid-day.

The oracle is the venue's truth log: one line per disseminated message keyed by its MoldUDP64 sequence — the fields as
they went on the wire and the issue's day summary after it. The witness (`platform/spec/WITNESS.md`, "FINRA TRACE") is
one line per message the reader applied, joined on CUSIP + trade id, carrying the decoded fields and the reader's day
state per Section 8.3.8 (move the fields the Change Indicator names). Thirteen scripted cases plus `manual` (control
`udp://127.0.0.1:28989`: `cancel N`, `correct N`, `halt CUSIP`, `close`, the packet faults, `mark`).

The reference reader is the credit demo's `trace_md`, rebuilt from a to-shape prototype to the specification for this
tool (with `trace_witness` writing the witness from its shared-memory channel). The first run found the venue's
end-of-session packets going out as heartbeats — the MoldUDP64 packet builder rewrote the count on send — fixed in the
venue; the reader was right.

## 3q · The SIPs: CTA (`cta-sip-verifier`) and UTP (`utp-sip-verifier`) — the consolidated tape

A third kind of tool, **tape**: a SIP has no order entry, so the question is whether your consolidated-feed handler
holds the National BBO, the LULD bands, the halts and every off-exchange print the way the plans define them. Both
lanes are built from the plans' own documents — CTA: CQS + CTS Pillar Output v2.11b, the Common IP Multicast
Distribution Network spec, the Retransmission and Snapshot User Guide v1.8, CQS Pillar Snapshot v3.1; UTP: the Data
Feed Services Specification v4.1 (UQDF + UTDF binary over MoldUDP64), UTP Snap-Shot v4.0 over SoupBinTCP — against one
simulator each (`cta_me`, `utp_me`) playing seven participants, the FINRA ADF and the TRFs.

**The witness** is the feed-handler contract with one twist: each level is the total size of the NBBO-eligible
protected quotes at that price across participants, and **level 1 must equal the SIP's National BBO** (also
cross-checked against the NBBO the tape carried in its appendage). CTA: CQS `233.54.29.121:25287`, CTS
`233.54.29.123:25288`, retransmission group `233.54.29.122:25286`, request server `tcp://127.0.0.1:25289`
(login / retransmission / snapshot per the User Guide), SPY / JPM / XOM (locate 1 / 2 / 3), control `udp 25290`.
UTP: `233.54.30.121:25187` (MoldUDP64 session `UTPBENCH01`, one channel for UQDF and UTDF), retransmission
`udp://127.0.0.1:25188`, Snap-Shot `tcp://127.0.0.1:25189` (SoupBinTCP login at sequence 1), AAPL / MSFT / TSLA,
control `udp 25190`.

**Cases** (both tools): the six transport cases (`clean` = the late join from the snapshot, `drop_single`,
`gap_burst`, `drop_sustained`, `duplicates`, `reorder`) and five tape cases judged from your handler's log against the
truth — `luld_bands` (every band logged, the NBBO inside the band in force), `halt_resume` (the halt, the cleared
quotes, the resume; quotes during the halt leave the NBBO blank), `off_exchange_prints` (ADF / TRF prints published as
kind 1 and never moving the NBBO), `trade_corrections` (each cancel / error and correction logged with its reference),
`mwcb` (a level-1 breach: the status message, every symbol halted and resumed) — plus `manual` (`halt SYM · resume SYM
· band SYM · mwcb LEVEL · trf N · cancel N · correct N` on top of the usual fault commands). Log lines the report reads:
`band SYM low high`, `mwcb level N`, `cancel_error <id>`, `correction <id>`, and the status lines.

The UTP bench found a defect in the desk's trader rather than the handler: a started strategy quoting a symbol whose
exchange has no venue adapter dereferenced a null symbol trader and crashed `bts_trader` — fixed the same day with a
local reject, and both SIP UAT packs now start their makers to prove it.

---

## 4 · Let your coding agent do it (MCP)

Register the toolbox in your agent (Claude Code, Cursor, …):

```json
{"mcpServers": {"mts-toolbox": {"command": "docker",
  "args": ["run", "--rm", "-i", "--net", "host", "-v", "mts-toolbox-data:/data/toolbox", "mts-toolbox", "mcp"]}}}
```

Then ask it: *"Verify my MDP 3.0 handler with the mts-toolbox tools."* It calls `list_tools` → `get_contract`
→ `start_venue`, writes the witness adapter against your code, runs your handler, `submit_input` →
`stop_venue` → `report`, and reads the verdict. Your code stays on your host.

---

## 5 · The GUI

Everything above can be driven from a browser instead of the CLI:

```
docker run --rm --net host -v mts-toolbox-data:/data/toolbox mts-toolbox serve     # http://localhost:8790/
```

Open an app: pick the case, **Start venue**, watch the live counters, **Drive it** (one button per control
command — pause, drop N, withhold N burst, terminate, mark …, plus a free command box and the command log),
**Your side** (upload the witness or your log), **Report this case** (the verdict table and, with marks, the
phase table), or **Run our reference client**. The page is your host's gallery — results are your runs.
Both the CLI and the GUI can drive a venue the other started (they share the data volume).

## 6 · Results and files

`/data/toolbox/<tool>/<case>/` (in the volume): `report.md`, `report.json`, the venue's logs and truth, your
inputs. `mtsx report <tool> all` writes a combined report. `mtsx serve` (add `-p 8790:8790`) shows a local
gallery with your results.

---

## 7 · Troubleshooting

| symptom | cause · fix |
|---|---|
| `Unable to find image 'mts-toolbox:latest'` | an older tarball had only the `:0.1` tag — `docker tag mts-toolbox:0.1 mts-toolbox:latest`, or re-download |
| `mtsx doctor` fails the multicast row | the interface does not loop multicast: on Linux loopback it works out of the box; for a LAN interface check `ip maddr`, firewalls, and that nothing blocks `239.0.0.0/8` |
| your handler sees nothing | it joined the group on the wrong interface (must be the venue's: loopback by default, `CME_IFACE` otherwise); or a firewall; run `reference cme-md-verifier clean` — if it passes, the venue is fine |
| a port is in use | a venue is still running: `mtsx stop <tool> <case>` |
| `witness_present` fails | the witness file is empty or its `seq` values are not the packet sequence numbers |
| every book mismatches at the same levels | check `New` drops the level beyond the depth, `Delete` shifts up, and quantities are exact |
| gateway cases: `gap_detected` fails | your gateway sat on a quiet session: gaps are also signalled by `Sequence.NextSeqNo`; and your trader must keep sending during the withheld window |
| macOS / Windows | Docker Desktop has no host networking for multicast — use a Linux host or VM |

---

## Layout (repo)

```
platform/   mtsx (CLI) · toolbox/{core,mcp_server,web}.py · spec/{TOOL,WITNESS,MCP}.md · gallery/index.html
tools/      cme-{md,oe,dc}-verifier/  nsdq-{md,oe}-verifier/  nyse-{md,oe}-verifier/  arca-{md,oe}-verifier/  bzx-{md,oe}-verifier/  cfe-{md,oe}-verifier/  coinbase-{md,oe}-verifier/  kraken-{md,oe}-verifier/  binance-{md,oe}-verifier/  okx-{md,oe}-verifier/  hyperliquid-{md,oe}-verifier/  dydx-{md,oe}-verifier/  injective-{md,oe}-verifier/   (tool.json · cases.json · venue.sh · status.sh · reference.sh · the report)
examples/   witness/ (the MD witness helpers) · py_handler/ (an independent MDP 3.0 handler) · order_witness/ (the order witness from a gateway log: CME, Nasdaq, NYSE / Arca, Cboe BZX / CFE, Coinbase, Kraken, Binance, OKX, Hyperliquid, dYdX, Injective)
docker/     Dockerfile · build.sh (stages BTS2 release binaries; no source ships) · sync.sh · export_public.py
```

Adding a tool = a directory with a manifest ([platform/spec/TOOL.md](platform/spec/TOOL.md)); the platform
does not change.

## Licence

Evaluation and non-commercial use; see [LICENSE.md](LICENSE.md). Commercial licences, certification packs
and onboarding engagements: through [priorstates.com/mts](https://priorstates.com/mts/).

**Provided as is — no warranty, no liability for trading losses.** A tool's verdict is evidence about your software
against a simulated venue, not an exchange certification and not a guarantee of production behaviour; the author is
not liable for any loss arising from the software or its results, including trading losses, erroneous or duplicated
orders, missed executions or exchange penalties. You are solely responsible for testing, certifying and operating your
trading systems. The full terms are in LICENSE.md.
