Skip to main content

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 path backend/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).
  • ExecutionReport grew a v2 tail: feeMicros (u64, fee charged for THIS execution in micro-USDC) and transactTimeNs (u64, engine apply timestamp in unix nanos); blockLength grew 187 → 203. Gate on blockLength >= 203, never on version — 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.

MessagetemplateIdgolden hex (SOFH-framed)
NewOrderSingle1000000885be07a00010001000200434f4e462d31…010201000000
MassQuoteRequest6000001585be05c00060001000200434f4e462d4d512d31…0202010000
OrderMassStatusRequest70000006b5be05d00070001000200434f4e462d4d532d31…0000000000
ExecutionReport10000000db5be0cb000a0001000200abab…fa000000000000000068e5cf8b0100000000
Negotiate1000000002d5be01900640001000200111111…030400434f4e46
Establish103000000385be02800670001000200111111…01000…00
EstablishmentAck104000000365be02800680001000200111111…0100…00
Sequence106000000165be008006a00010002000100000000000000

(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:

  1. Encodingverify-golden green (all golden vectors byte-identical).
  2. Handshake — Negotiate → NegotiationResponse, Establish → EstablishmentAck; Recoverable flow required (non-Recoverable is rejected).
  3. Order lifecycle — NewOrderSingle → ExecutionReport drop-copy; OrderCancelRequest and OrderCancelReplaceRequest by OrderID and by OrigClOrdID.
  4. Idempotency — a duplicate ClOrdID placement is rejected (BusinessMessageReject, businessRejectReason=3).
  5. Mass quote — a two-leg MassQuoteRequest acks with one PendingReplace ExecutionReport, and each placed leg is cancelable by "<clOrdId>:<i>"; a batch with an unknown origClOrdId is rejected whole (businessRejectReason=4, nothing applied).
  6. Recovery — on a sequence gap the server emits RetransmitRequest; a client RetransmitRequest is answered with Retransmission + replayed frames. After a reconnect, reuse the same sessionId; the gateway resumes the retained outbound replay buffer only after the new Negotiate authenticates the same account.
  7. LivenessSequence keepalives are exchanged on the negotiated interval; a silent connection is closed after the grace window.
  8. Rate limits — sustained order flow stays within the per-account budget (shared MM limiter; a multi-leg MassQuoteRequest costs one unit per leg).

Current support matrix

CapabilityStatus
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)

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:

  • origClOrdId non-empty → the cancel resolves through the session order-ref store; cancelOrderId non-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 origClOrdId rejects 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).

CodeMeaning
1malformed / undecodable application message
2order account does not match the authenticated session
3duplicate ClOrdID
4unknown or unresolved OrigClOrdID
5stored order id not resolvable
6session not established for ClOrdID resolution
7submit rejected (engine/risk reason in text)
9mass quote requires at least one entry
10order status: order not found
11order status: not authorized for this account

Cancel-on-disconnect & mass-cancel (MM risk controls)

  • Cancel-on-disconnect is armed per credential (cancelOnDisconnect on 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 ExecutionReport with ExecType=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, fallback GATEWAY_RESUME_TTL_S, default 30s). Reconnect with the same sessionId, authenticate the same account in Negotiate, complete Establish, then send RetransmitRequest for the missing server sequence range. A different authenticated account using the same sessionId is rejected instead of receiving a fresh or resumed session.
  • Liveness as a soft dead-man's-switch: stop sending Sequence keepalives and the session dies on the negotiated interval, which triggers the cancel-on-disconnect sweep.