Binary-FIX (SBE + SOFH + FIXP) — Conformance & Certification
Senticore's binary-FIX gateway is a CME iLink-3-class stack:
- SBE (Simple Binary Encoding) — fixed-layout binary messages from the published schema
(
GET /api/v1/fixp/schema, repository pathbackend/protocol/sbe-fix/schema/senticore-orderentry-1.0.xml,schemaId=1). - SOFH (Simple Open Framing Header) — 6-byte big-endian frame delimiter.
- FIXP (FIX Performance session layer) — binary Recoverable-flow session (Negotiate / Establish / Sequence / RetransmitRequest / Retransmission / Terminate).
Application messages (templateId 1–12) and FIXP session messages (templateId 100–111) ride the same SOFH stream, distinguished by templateId.
Start from GET /api/v1/fixp/connectivity. The bundle includes fixp.enabled and
fixp.wire.schemaUrl, currently /api/v1/fixp/schema. Clients should treat
fixp.enabled=false as "do not connect" and fall back to FIX 4.4 or BSL.
Wire framing
SOFH (6 bytes, BIG-endian):
u32 message_length # total bytes INCLUDING these 6
u16 encoding_type # 0x5BE0 = SBE little-endian
SBE message header (8 bytes, LITTLE-endian):
u16 blockLength # fixed root block size
u16 templateId
u16 schemaId # = 1
u16 version # = 2
<root block: blockLength bytes> [ <repeating groups> ] [ <var-data: u16 length + bytes> ]
schemaId is fixed forever. version is bumped only for additive, backward-compatible changes
(append trailing fields, grow blockLength; old decoders skip via blockLength). Breaking changes
ship as a new schemaId on a new port, with both running during migration.
Schema v2 (semanticVersion 1.1), additive:
MassQuoteRequest(templateId 6) — atomic multi-leg quote replace (see below).ExecutionReportgrew a v2 tail:feeMicros(u64, fee charged for THIS execution in micro-USDC) andtransactTimeNs(u64, engine apply timestamp in unix nanos);blockLengthgrew 187 → 203. Gate onblockLength >= 203, never onversion— v1 frames (blockLength=187) remain valid and decode with both fields defaulted to 0. Encoders may keep emitting v1-shaped reports; the gateway emits v2.
Session authentication
Credentials are presented once, at FIXP Negotiate, in the credentials var-field:
apiKeyId:apiSecret:apiPassphrase:accountHex
verified against your issued agent credential (the same FixLogon credential as tag=value FIX).
After Establish there is no per-order signing — the session is the authenticated identity.
Every order's embedded account field must equal the authenticated account (cross-account orders
are rejected). Credentials travel in cleartext inside TLS; the gateway refuses plaintext in
guarded environments.
Golden vectors
The reference encoder (scripts/fixp_conformance_smoke.py) and the Rust codec
(backend/gateway/fixp/tests/golden.rs) emit these byte-identical full-frame (SOFH-wrapped) hex
vectors. Your codec must reproduce them exactly. Canonical inputs: account = 0x000102…13,
orderId = 0xAB×32, sessionId = 0x11×16, timestamp = 1700000000000.
| Message | templateId | golden hex (SOFH-framed) |
|---|---|---|
| NewOrderSingle | 1 | 000000885be07a00010001000200434f4e462d31…010201000000 |
| MassQuoteRequest | 6 | 000001585be05c00060001000200434f4e462d4d512d31…0202010000 |
| OrderMassStatusRequest | 7 | 0000006b5be05d00070001000200434f4e462d4d532d31…0000000000 |
| ExecutionReport | 10 | 000000db5be0cb000a0001000200abab…fa000000000000000068e5cf8b0100000000 |
| Negotiate | 100 | 0000002d5be01900640001000200111111…030400434f4e46 |
| Establish | 103 | 000000385be02800670001000200111111…01000…00 |
| EstablishmentAck | 104 | 000000365be02800680001000200111111…0100…00 |
| Sequence | 106 | 000000165be008006a00010002000100000000000000 |
(Full untruncated vectors are in EXPECTED_HEX in the Python script and EXPECTED_* in golden.rs.)
The TypeScript SDK asserts the same vectors through encodeFixpNewOrderSingle,
encodeFixpMassQuoteRequest, encodeFixpNegotiate, encodeFixpEstablish,
encodeFixpSequence, and decodeFixpFrame (including v1-ExecutionReport backward decode).
Reference client
Stdlib-only Python, runnable in restricted certification environments:
# 1) prove your/our encoder is byte-compatible with the Rust codec
python scripts/fixp_conformance_smoke.py verify-golden
# TypeScript SDK equivalent
cd sdks/typescript && npm test -- --test-name-pattern FIXP
# 2) drive a live handshake + order against a gateway
python scripts/fixp_conformance_smoke.py session \
--host fix-bin.example --port 9879 --tls \
--api-key spk_… --api-secret sps_… --api-passphrase spp_… \
--account 0xabc… --market 1 --side bid --price 1000000 --qty 1
Certification checklist
An MM is certified once it passes:
- Encoding —
verify-goldengreen (all golden vectors byte-identical). - Handshake — Negotiate → NegotiationResponse, Establish → EstablishmentAck;
Recoverableflow required (non-Recoverable is rejected). - Order lifecycle — NewOrderSingle → ExecutionReport drop-copy; OrderCancelRequest and OrderCancelReplaceRequest by OrderID and by OrigClOrdID.
- Idempotency — a duplicate ClOrdID placement is rejected (
BusinessMessageReject,businessRejectReason=3). - Mass quote — a two-leg
MassQuoteRequestacks with one PendingReplace ExecutionReport, and each placed leg is cancelable by"<clOrdId>:<i>"; a batch with an unknownorigClOrdIdis rejected whole (businessRejectReason=4, nothing applied). - Recovery — on a sequence gap the server emits
RetransmitRequest; a clientRetransmitRequestis answered withRetransmission+ replayed frames. After a reconnect, reuse the samesessionId; the gateway resumes the retained outbound replay buffer only after the newNegotiateauthenticates the same account. - Liveness —
Sequencekeepalives are exchanged on the negotiated interval; a silent connection is closed after the grace window. - Rate limits — sustained order flow stays within the per-account budget (shared MM
limiter; a multi-leg
MassQuoteRequestcosts one unit per leg).
Current support matrix
| Capability | Status |
|---|---|
| NewOrderSingle / Cancel / CancelReplace (by OrderID or OrigClOrdID) | ✅ |
| Priority-preserving modify: qty-down replace keeps queue position (v2) | ✅ |
| MassQuoteRequest — atomic multi-leg quote replace (v2) | ✅ |
| OrderMassStatusRequest — open-orders snapshot with terminal marker (v2) | ✅ |
ExecutionReport drop-copy (v2: per-execution feeMicros + transactTimeNs) | ✅ |
| Duplicate-ClOrdID rejection | ✅ |
Numeric businessRejectReason codes + refMsgType on every reject (v2) | ✅ |
| FIXP Negotiate/Establish/Sequence/RetransmitRequest/Retransmission/Terminate | ✅ |
| OrderMassCancelRequest (cancel-all / per-market / per-side) | ✅ |
| Kill-and-block: MassCancelRequestType 100/101 (sweep + persistent trading block) | ✅ |
| Cancel-on-disconnect (resting orders swept on any teardown) | ✅ |
| Reject-on-unsupported (no silent semantic downgrade) | ✅ |
Public TCP/TLS reachability with FIXP Negotiate session auth when fixp.enabled=true | ✅ |
| Optional source-IP allowlist / mTLS admission control for dedicated routes | ✅ |
| OrderStatusRequest reply by OrderID or session ClOrdID | ✅ |
| Cross-connection resume (bounded ResumeRegistry) | ✅ |
Priority-preserving modify (iLink semantics)
A cancel/replace that provably changes NOTHING but the quantity downward — same market,
same side, same book, same limit price, 0 < newQty <= remaining against the live resting
order — is applied as an in-place amend: the engine order id does not change and the order
keeps its queue priority. The ack is ExecType=Replaced carrying the unchanged order id;
the new ClOrdID becomes the order's session handle (the superseded one is marked replaced).
Any other change (price move, qty up, market order, qty 0) is an atomic cancel+replace and
re-queues as a NEW order id. Identical behavior on FIX 4.4 (35=G) and FIXP
(OrderCancelReplaceRequest); on BSL use the compact AMEND frame directly. A MassQuoteRequest
always re-queues its legs (cancel+place) — use single replaces when priority matters.
Open-orders snapshot (OrderMassStatusRequest / FIX 35=AF)
One request returns the account's resting orders from the live engine snapshots — one
ExecutionReport (ExecType=OrderStatus) per order (optionally filtered by marketId
and/or side), each resolving the session ClOrdID where known — followed by a terminal
marker report: all-zero orderId, text = "mass_status_end;total=<n>". Use it to rebuild
order state after a reconnect that outlived the resume window.
Kill-and-block (MassCancelRequestType 100 / 101)
530=100(kill-and-block): the account's trading block is latched FIRST (persistent — survives restarts), then ALL open orders are swept (filters are intentionally ignored). While blocked, every risk-increasing action (placements, quote-replace legs with qty > 0) is rejected at admission on the FIX and FIXP lanes; cancels and qty-down amends stay allowed so the account can always flatten. FIXP acks with a marker report (text = "kill_and_block;canceled=<n>"); FIX 4.4 acks with the MassCancelReport (35=r).530=101(re-enable): clears the latch, cancels nothing, acks (text = "trading_enabled"on FIXP).- Scope note: the block is enforced on the FIX/FIXP admission path today. The kill SWEEP removes resting orders from every lane (orders are lane-agnostic); blocking NEW orders on the BSL and HTTP order-entry admission is a coordinated follow-up.
Per-fill fees on every lane
The per-execution fee is reported on all lanes: FIXP v2 feeMicros, FIX 4.4
Commission(12) in micro-USDC with CommType(13)=3, WS/SSE drop-copy feeMicro.
MassQuoteRequest (atomic multi-leg quote replace)
One MassQuoteRequest = ONE sequenced engine action: all cancels and placements across its
quoteEntries apply atomically (a two-sided re-quote can never be half-applied). Per entry:
origClOrdIdnon-empty → the cancel resolves through the session order-ref store;cancelOrderIdnon-zero → cancels that engine order id directly (wins); both absent → place-only leg.orderQty=0→ cancel-only leg.- The i-th placed leg's child ClOrdID is
"<clOrdId>:<i>"(0-based) and is registered in the order-ref store, so every placed quote is individually cancel/replace-able by ClOrdID. - One unresolved
origClOrdIdrejects the WHOLE batch (businessRejectReason=4) — nothing reaches the engine. - The synchronous ack is a single
ExecutionReport(ExecType=PendingReplace) carrying the batch ClOrdID and the first placed leg's order id; per-leg terminal state (fills, cancels) arrives via drop-copy. Rate limiting counts per leg, not per frame.
Business reject reason codes (businessRejectReason)
Stable numeric codes; match on these, not on text. refMsgType carries the FIX MsgType of
the rejected message (D, F, G, q, H, i).
| Code | Meaning |
|---|---|
| 1 | malformed / undecodable application message |
| 2 | order account does not match the authenticated session |
| 3 | duplicate ClOrdID |
| 4 | unknown or unresolved OrigClOrdID |
| 5 | stored order id not resolvable |
| 6 | session not established for ClOrdID resolution |
| 7 | submit rejected (engine/risk reason in text) |
| 9 | mass quote requires at least one entry |
| 10 | order status: order not found |
| 11 | order status: not authorized for this account |
Cancel-on-disconnect & mass-cancel (MM risk controls)
- Cancel-on-disconnect is armed per credential (
cancelOnDisconnecton the agent). When enabled, every resting order placed on the session is registered, and on ANY connection teardown — clean close, timeout, network drop, or error — all of that account's resting orders are swept. A dropped TCP connection never leaves a live quote stack in the book. - OrderMassCancelRequest cancels all open orders for the account in one message — optionally filtered to specific markets and/or a side — fanned out as qty=0 quote-replaces by the engine. This is the low-latency panic-kill; you do not enumerate orders client-side.
- OrderStatusRequest is read-only. If the order is in the live engine
snapshot, the gateway returns an
ExecutionReportwithExecType=OrderStatus. If the request references a session ClOrdID that is still pending/replaced/ canceled in the order-ref store, the gateway returns that session-scoped status instead of going silent. - Cross-connection resume is account-bound. A disconnected established
session parks its outbound retransmit buffer for the configured retention
window (
FIXP_RESUME_TTL_S, fallbackGATEWAY_RESUME_TTL_S, default 30s). Reconnect with the samesessionId, authenticate the same account inNegotiate, completeEstablish, then sendRetransmitRequestfor the missing server sequence range. A different authenticated account using the samesessionIdis rejected instead of receiving a fresh or resumed session. - Liveness as a soft dead-man's-switch: stop sending
Sequencekeepalives and the session dies on the negotiated interval, which triggers the cancel-on-disconnect sweep.