Skip to navigation

Market Data WebSocket changelog

Consumer-facing history of the client wire (asyncapi.yaml is the contract; this file says what changed, for the people who parse it). Every client-visible change has its section here, newest first, each stating what each side of the deploy window sees.

The composite channels — Binance spot rejoins: its connector states each value’s minimal scale

What changed

  • Nothing in the gateway, its schema or its frames. What moved is upstream, in binance-md-connector, which no longer states the eight places Binance renders on every surface as the instrument’s precision. Every price and size is now read at its minimal scale — the trailing zeros stripped, 64887.68000000 read as 64887.68 at 2 — and stated, as every moved connector states a value, at its channel’s working scale, which therefore settles at the instrument’s grid instead of at eight: a BTCUSDT book is stated at one pair, 2/5 (its 0.01 tick and 0.00001 step), instead of 8/8, inside the record’s 4/7, and an ETHUSDT book at 2/4 inside its 4/6. The rule is the venue’s, chosen because Binance’s width carries no information about the instrument: a venue that pads to its grid (Kraken, Bybit) is unchanged.
  • Every Binance spot book enters the merge. A composite holding a Binance spot source beside others stops naming it precision and merges its levels, and one built on Binance spot alone serves. The rejoin is the one the re-cut’s section below (a precision exclusion reason) defines: once the source’s next snapshot is stated within the topic’s output scales it is named in a recovered frame — whether it had left the merge or, as every Binance spot source since its move, had never entered it — and appears in src from the next full image. A topic on Binance spot alone that acked syncing and never served says recovered, then serves.
  • precision no longer stands for padding. A source is told precision because its book is stated finer than the topic’s output scales, and the two ways that happens are the ones already in this file: the venue outgrew its reference record (the precision is reachable section), or — after a record narrows — a running connector keeps stating the finer scale it had learned until it restarts (the transient cause the section directly below names). Never any more because a venue renders more digits than it trades.
  • The per-instrument channels are unchanged. They render shortest form and always did, so 64887.68000000 read as 64887.68 before this release and reads the same after; no digit a client saw changes. What a client can notice: agg_book levels attributed to a Binance spot source appear where before the source was absent; and on book, raw or conflated, a Binance spot book’s restatements now come from real precision moves rather than never — over a connector’s first frames after a restart a book channel can widen as it climbs to the grid, and each book-channel widen is one more snapshot: true frame, the form the contract has always used. Expect none in the common case: the venue’s capture widened twice, both on a trade channel, which emits no book frame.
  • rescaled is unchanged by this, and no reference record moves: the alternative — widening the record to the venue’s rendering once a record’s pair can move — was not taken, so no composite holding a Binance spot source is rescaled to eight places.

Deploy window: nothing in the gateway deploys for this section; binance-md-connector does, and its restart is the window. A gateway beside the connector before its deploy is the precision is reachable section’s world for this venue: every Binance spot book out of every composite as precision, a composite on Binance spot alone syncing and never serving. After: the connector’s next snapshot of each book is stated at the minimal pair, and every composite holding that source says recovered — or serves for the first time — and merges it at its next tick. Nothing a client must do: one that already treats precision as a standing word and parses decimal strings without assuming a digit count sees a source appear, and that is all. The gateway has no “before” of its own here, for the reason the precision is reachable section gives.

The composite channels — rescaled becomes ordinary: a source’s scales can now change

What changed

  • Nothing in the gateway, its schema or its frames — the re-cut’s section below taught the two words and they are unchanged. What changes is upstream, and it changes when a client hears one of them. Until now the reference-data owner froze each instrument’s scale pair at first listing and refused any catalog row that needed another, so a composite’s output scales — derived from its sources’ records — could only move in an incident. From this release the owner derives the pair from the catalog row at every poll and publishes a changed pair as the record’s next version. A topic’s output priceScale / qtyScale can therefore move under a live subscription in normal operation, and status: "rescaled" is how the client is told — one frame per affected topic per change, before the first frame rendered at the new pair, exactly as the re-cut’s section below specifies. Never per tick.
  • Both directions happen. The scales widen when a source venue starts quoting finer than the catalog had registered and the catalog catches up. They can narrow when a venue coarsens its price or size grid, or when a first registration that was too fine is corrected. A client must not assume a rescaled pair is at or above the one it replaces.
  • A narrowing can leave a vol_book ladder behind — and a re-subscribe can then answer bad_ladder. bucketSizes are held at the topic’s quantity scale, so a rescale restates them exactly or not at all. A widening always can. A narrowing cannot when a bucket size uses a decimal the narrower scale drops ("2.55" at a quantity scale going from 2 to 1): that topic keeps the pair it advertised — still exact, and no rescaled is sent for it — but the same config sent again later (a reconnect re-subscribes with the full config) is judged against the current scale and refused bad_ladder, naming the scale. Send bucket sizes no finer than the instrument’s size increment and neither can happen.
  • Expect a burst once, then rarity. Be ready for rescaled on many topics at the first catalog poll after this release deploys, and for it to recur, rarely, over a subscription’s life — it is not a once-in-a-listing event. The burst: every instrument whose pair had been held at a stale first sighting corrects at once, and each affected topic — one whose output pair moves with it — says rescaled then; a source’s pair can change under an output pair another source still sets, and then nothing is said. The rarity: after that a topic’s scales move only when a source’s catalog row stops fitting the scales its record holds — a venue making its grid finer than the record’s headroom absorbs, or a registration being corrected — while a one-decimal tick move is absorbed and says nothing (a week of published catalog history, 2026-09-20, held no other kind of move).
  • reason: "precision" is live already, and gains one transient cause. The re-cut’s section below said precision could not be sent until a later release let a connector state a venue’s own digits; the section directly below is that release, and the connectors moved to it venue by venue — every market-data connector states the venue’s own digits now, and that section names them. For a moved connector precision means what the re-cut’s section says: the source’s book is stated finer than the topic’s output scales, which derive from the reference records. Two things clear it. The record widens — which, from this release, the owner does at the next catalog poll that shows the finer precision, nobody acting — or the venue’s rendering changes. (A venue that padded its rendering past its own grid — Binance spot, eight places on every value above its record — stayed precision on either side of this until its connector stripped the padding: the section above, which lands in the same release.) A narrowing can put a source out until its connector restarts. A stated scale never narrows within a connector’s process — the rule the section below states — so after a record narrows, a connector whose channel had learned digits above the new pair keeps stating them, every value still exact. If that leaves the source above a topic’s output scales, it is announced out of the merge by an ordinary degraded frame with reason: "precision", and rejoins through an ordinary recovered frame when the connector restarts and its channels learn the venue’s digits afresh. Nothing is mis-rendered in between: a level is never rounded into a merge. It takes a venue coarsening by more than the record’s headroom, so expect it rarely.

Deploy window: the gateway and the reference-data owner deploy independently, and only the owner changes here. A gateway carrying the re-cut’s section below, beside an owner before this release: rescaled only in an incident, as that section says; precision already, from the connectors that state a venue’s own digits — and standing wherever a venue has outgrown a record the owner still refuses to widen. The same gateway beside an owner at this release: rescaled on each affected topic whenever a source’s pair change moves its output pair; precision clearing within a poll of the catalog showing the finer precision; and the transient cause above. There is no third combination — a gateway older than the re-cut’s section below cannot read the re-cut market-data schema this owner’s estate runs on. What a client must do is what that section already asked: parse decimal strings rather than assuming a digit count; if it formats to the ack’s priceScale / qtyScale, take the new pair from the rescaled frame — in either direction; treat an unknown status as ignorable and an unknown reason as stale.

The composite channels — precision is reachable: the connectors state the venue’s own digits

What changed

  • Nothing in the gateway, its schema or its frames. No field, enum member or frame shape moves, and this section adds no word the section below did not teach. What moved is upstream, in the connectors, and it makes one sentence of that section true sooner than it said: reason: "precision" “cannot be sent until a later release lets a connector state a venue’s own digits”. This is that release — it carries the section below’s re-cut and the connectors’ move together, so a client meets both at once.
  • Which sources can now be announced precision, and why. A moved connector states every price and size at the digits the venue renders on that channel — a working scale learned from the wire — where before it stated the instrument’s reference-record pair on every value. A composite’s output scales are still derived from its sources’ records (the priceScale / qtyScale the ack echoed), and each source is judged on the pair its book was last stated at. A record’s pair is derived with headroom — at least two places past the instrument’s tick and past its step — so a venue that renders inside that headroom enters the merge as it always did, and one that renders past it is out as precision. One kind of venue does: one that outgrew its record — it re-gridded an instrument finer than the record’s headroom absorbs. (A venue that padded its rendering past its own grid, Binance spot, did too between its move and the section at the top of this file, which strips the padding at the connector.) The sources this can be said of are the moved venues’ books — every market-data venue’s, since this release moves them all: Kraken, Coinbase, Gate.io, Bybit, Binance futures, Binance spot, OKX, KuCoin futures and MEXC. KuCoin spot moved too but publishes no book yet, so none of its instruments is a composite source.
  • How it clears. As the section below says, the source rejoins through an ordinary recovered frame once its book is within the topic’s scales again, and two things get it there. The record widens: the catalog restates the instrument’s pair, the gateway rebinds every composite holding that source, says rescaled with the new pair wherever the output scales moved, and the source rejoins. Today that widening is an operator’s act — the reference-data owner refuses a changed pair in normal operation, so a record moves only when the instrument is re-listed, the incident shape the section below names — and a precision source waits on it. The release that makes the pair a versioned attribute turns the wait into a poll: the owner derives the pair from the catalog row at every poll and publishes the wider pair as the record’s next version, so the source rejoins within a poll of the catalog showing the finer precision, nobody acting — that release’s own section says what else it changes. The venue’s rendering changes: a stated scale never narrows within a connector’s process — a venue that coarsens its digits, or stops padding, keeps being stated at the finer pair, exactly, with trailing zeros — so a coarser rendering reaches the gateway at that connector’s next restart, when its channels learn the venue’s digits afresh and the source’s next snapshot states the coarser pair.
  • Binance spot stood excluded from its move until the section at the top of this file. Binance spot renders eight places on every surface whatever the instrument’s grid — BTCUSDT trades on a 0.01 tick and a 0.00001 step, and every price and size still reads 64887.68000000 and 0.00200000 — and its moved connector stated 8/8 on every channel while the record derives 4/7, so every Binance spot book was out of every composite as precision from its first tick, and a composite on Binance spot alone never served. The fix is at the connector, in the same release as this section — the section at the top of this file: the connector states each value’s minimal scale, its books settle at the grid (BTCUSDT 2/5) and enter the merge with the topic’s scales where they are, so no client meets the exclusion.
  • The per-instrument channels are unchanged in shape and, nearly everywhere, in content. trade, tob, book, mark, oi, candlestick and ticker advertise no scale and render shortest form, so a value the venue pads reads as it did — Binance spot’s 64887.68000000 was 64887.68 before the move and is after — and a level’s src attribution on agg_book, rendered from the venue’s stated pair, is the same string at any stated scale. Two things a client can notice. A value the record’s grid could not hold was quarantined by the unmoved connector — never published — and a moved connector publishes it exactly, so on a venue that outgrew its record a client sees prices or sizes at more decimals than the record’s pair where before it saw nothing. And when a moved connector’s book channel learns a finer digit than it had seen, it restates the held book at the finer pair before deltas resume: on book, raw or conflated, that is one more snapshot: true frame — the form the contract has always used for a restatement — carrying the same digits. On a shortest-form venue (Coinbase, Gate.io) expect those over a connector’s first frames after a restart, then rarely.
  • rescaled is unchanged by this: still only the reference-data incident the section below names, until the release that lets a record’s pair move.

Deploy window: nothing in the gateway deploys for this section; the connectors do, one venue at a time, and each is its own window. A gateway carrying the section below beside a connector before its move is that section’s world for the venue: the connector states the record’s pair, so the venue’s sources sit at or below the output scales by construction, precision is unreachable in normal operation, and a value off the record’s grid does not reach the wire. The same gateway beside a connector after its move: that venue’s books, where it publishes one (KuCoin spot does not yet), can be told precision under the rule above, and the per-instrument channels carry every value the venue renders, exactly. The gateway has no “before” of its own here: a gateway older than the section below cannot read the re-cut market-data schema the moved connectors publish on (a clean break under a schema floor, taken with an environment reset), so it never runs beside one. What a client must do is what the section below already asked — treat an unknown reason as stale, and parse decimal strings rather than assuming a digit count — plus one thing new with this section: expect precision to be a standing word, not a transient one. A source told precision waits on the catalog or on the venue, not on a restate interval.

The composite channels — a precision exclusion reason, a rescaled status, and one rule for ts

What changed

  • Two new enum members on agg_book and vol_book status frames. The exclusion reason vocabulary gains precision, and the status vocabulary gains rescaled. Both enums were documented closed (“complete so consumers bind once”), so a client that generated an exhaustive enum from the spec must regenerate — or, better, treat an unknown reason as it treats stale and ignore an unknown status, which was always the safe reading.

  • reason: "precision" rides an ordinary degraded frame: the named source’s book is stated at more decimal places than the topic’s output scales (the priceScale / qtyScale the subscribe ack echoed) hold, so its levels cannot enter the merge without dropping digits, and a level is never rounded into a merge. It is judged on the scale, not on the digits the book happens to hold, so a source does not flap in and out with its content. The source rejoins through an ordinary recovered frame once its book is within the topic’s scales again: the topic was rescaled wide enough, or the venue’s next snapshot states a coarser scale. A rescale that widens only part of the way leaves it out.

  • precision is the one exclusion announced for a source that never entered the merge. A source that is merely not synced yet is covered by the ack’s syncing promise and joins silently, as it always has. A precision source is different — its book is servable, and it stays out until the catalog or the venue moves — so its degraded frame is sent once whether the source left the merge or was never in it. A composite whose only source is born that way answers syncing and then, at its first tick:

    {"event":"agg_book","topic":"agg_book.BTC-X.100ms","status":"degraded",
    "seq":"4242","seqTs":"1700000000000000000",
    "excluded":[{"s":"OKX@BTC/USDT","reason":"precision"}]}

    No ts — nothing is included to timestamp — and no stale marker after it, because the topic has not served yet and syncing still stands. A topic that was serving and loses its last source to precision gets the same frame first (naming every source that left with it) and then the stale marker, so the silence that follows has a stated cause.

  • precision is also the one reason said over another. A source already announced stale or fx_expired whose exclusion turns into precision — its book comes back from a resync, or its rate returns, and the book is finer than the topic — is named again in a degraded frame carrying the new reason. The first word promised a heal within a restate interval or a rate tick; this one waits on the catalog, and a client should not be left waiting on the wrong one. One recovered answers both. No other change of reason is re-announced: a precision source whose chain breaks and heals stays told precision.

  • status: "rescaled" is a new status frame with two new integer fields, priceScale and qtyScale — the ack’s own field names, carrying the topic’s new output scales:

    {"event":"agg_book","topic":"agg_book.BTC-X.100ms","status":"rescaled",
    "seq":"4242","seqTs":"1700000000000000000","priceScale":3,"qtyScale":2}

    The output scales are derived from the sources’ reference records. When a source’s record restates its scale pair — a venue starts quoting finer than the catalog had registered, and the catalog catches up — the gateway rebinds every composite holding that source, and because the client sent no request for an ack to answer, the frame says so. It is emitted once, at the tier’s tick, before the first frame rendered at the new pair (so before any recovered frame the rebind causes, and before the image). It carries no ts: it is a fact about the subscription, not about any source’s book. A rebind that leaves the output scales where they were says nothing, and so do two that land back on the pair the client was last told.

  • ts is written by one rule on every composite frame. It is the newest included source’s exchange timestamp: present when an included source carries one, absent otherwise — never "0". Images and the stale marker already behaved this way; degraded and recovered wrote "ts":"0" when no included source carried a timestamp, and now omit the field like the other forms. The schema never required ts.

  • Nothing else moves. Image frames, the ack, degraded / recovered / stale, every field name and type are unchanged, and every number on the topic is still a canonical shortest-form decimal string: a client that parses decimals — rather than assuming a digit count — sees the same values before and after a rescale. For vol_book, the ladder’s bucketSizes are restated at the new quantity scale, so the buckets a client subscribed to are the buckets it keeps receiving. An identical re-subscribe after a rescale is answered with the new pair. The per-instrument channels are untouched: the release that carries this section re-cuts the sequenced market-data schema underneath the gateway (every price and quantity now states its own scale), but trade, tob, book, mark, oi, candlestick and ticker advertise no scale and render shortest form, so every string on them is byte-identical before and after.

Deploy window: a gateway before the upgrade never sends either word, so a client built to this section meets nothing new against it and must simply not wait for the words. A gateway after the upgrade can send them to a client that has not learned them, but only when the data gives it cause, and through this release the data does not: every connector still states its reference record’s scale pair, which sits at or below every composite’s output scales by construction, so precision cannot be sent until a later release lets a connector state a venue’s own digits finer than its record; and rescaled needs a reference record whose pair changed, which the catalog refuses in normal operation until the release that makes a pair an attribute — the rescaled becomes ordinary section of this file, which also gives precision its one transient cause; the connectors that now state a venue’s own digits are named in the section directly above. The one way either happens earlier is an incident — a reference-data rebuild that re-lists an instrument at other scales — and there the frames are the better outcome: before this release the composite stayed bound to the pair the catalog had moved past and said nothing. That is the window to teach a client the two words. A client that has not learned them by then sees, at worst, a degraded frame whose reason it does not recognise (treat as stale: the source is out of the merge, and the image’s src says which sources are in) and a status frame whose status it does not recognise (safe to ignore if it parses decimal strings; a client that fixed its formatting to the ack’s priceScale / qtyScale must take the new pair from this frame). The ts correction is visible on both sides at once: an old gateway may send "ts":"0" on a degraded or recovered frame, a new one omits the field — treat ts as optional on every composite frame, which is what the schema has always said.

The ticker channel — turnover* is the contract-aware quote notional

What changed

  • turnover1h / turnover24h now carry the quote notional, not the bare price × quantity sum. For spot instruments nothing moves: the sum IS the quote notional. For contract-sized instruments the numbers change visibly: a linear contract (FUTURES, PERPETUAL_SWAP, QUANTO_*) is the sum times the instrument’s contractMultiplier (rounded half-even at priceScale + qtyScale, so the string keeps its scale — a 0.01-multiplier perpetual’s turnover is a hundredth of what it read before, which is what it was worth), and an inverse contract is volume × contractValue.
  • Null now also means “not valued”, beside the warming window it already meant: an instrument whose shape the rule cannot value — a linear contract with no stated multiplier, an inverse contract with no stated contract value (every inverse row today: reference data does not publish the field yet), an option, an index — renders null, never 0 and never the raw sum. The AsyncAPI description states the rule.
  • No field, type or frame-shape change: both fields stay [string, "null"] at the same scale; every other ticker field is untouched.

Deploy window: before the upgrade a contract-sized instrument’s turnover* read the raw contract-count sum (a mismatched unit — the correction this section lands); after, it reads the quote notional or null. A client that already treated null as “no value” needs no change; one that summed turnover across instruments of mixed shapes was summing mismatched units before and gets comparable quote amounts after. The candlestick channel carries no turnover field and is unaffected.

The ticker channel — trailing stats that decay on the stream frontier

What’s new

  • The ticker channel is served: ticker.SYMBOL[.tier] — last price plus derived trailing 1h/24h statistics off the rolling fold, re-rendered on trades (conflated per tier) and once per elapsed stream minute — the decay edge for quiet instruments; an actively trading topic simply receives one extra image at the minute edge. asOf (the consumed event-time frontier at render) advances on decay re-renders while seq/seqTs (fact provenance, the last trade folded) stay unchanged: asOf moving alone IS the decay tell.
  • Per-window cold-start completeness rides nullability: a window’s derived fields are null until that window’s history exists post-cold-start (between +1h and +24h the 1h numbers stand while the 24h ones are null); windowComplete is the 24h window’s flag; null never renders as 0, and a warm window with no trades is flat (zero sums, ratio "0", extremes carried), not null. pctChange* are ratios (not percent) at 4 decimals; turnover* are exact decimal strings at priceScale + qtyScale (sums never pass through floating point).

Deploy window: before this deploy, ticker.* topics answer per-topic errors — probe-and-fall-back works as for the candlestick channel’s section below.

The candlestick channel — closes on the stream frontier, and the object topic form

What’s new

  • The candlestick channel is served: candlestick.SYMBOL.INTERVAL[.tier], the interval segment mandatory from components.schemas.candleInterval (the 8-interval set shared with the candle-history surface). Subscribing answers live and pushes one snapshot: true frame — the last two closed bars plus the forming bar, oldest to newest; a never-traded instrument answers bars: [] with seq/seqTs omitted, so “no data exists” is distinguishable from “snapshot lost”. The raw lane streams a forming-bar partial per folded trade; tiers serve the latest forming bar per tick. Closes never conflate: every elapsed interval closes — zero-trade intervals as a carried close (OHLC = previous close, volume "0", count 0, provenance inherited) — released once the stream’s consumed event-time frontier passes the bucket boundary, never wall clock: under catch-up closes arrive late, never wrong. After closed: true for a bucket no further update frame for that bucket arrives, and the close precedes any frame of the next bucket. Around the subscribe itself, snapshot and topic broadcasts may overlap — re-applying a final keyed on the bucket timestamp is a no-op, never desync. seq/seqTs are fact provenance (repeats legal, gaps meaningless): apply keyed on the bucket timestamp.
  • Subscribe requests accept the object topic form (components.schemas.topicSelector): {channel, instrument, interval, throttle}, exactly those keys, string-valued, normalized and echoed as the dotted string — one ack shape with the string form. An unknown key or a non-string value (e.g. a numeric throttle) answers op error/malformed rather than silently arming different semantics. The required top-level instrument key is what distinguishes a selector from a composite entry (aggBookEntry/volBookEntry carry name/tier/sources and never a top-level instrument — an object with one always parses as a selector).

Deploy window: the old gateway answers every candlestick.* topic with a per-topic error, and mis-collects an object entry’s inner strings as individual topic strings, answering the subscribe ack with per-topic errors echoing those fragments (never malformed). A probing client should treat an ack whose per-topic errors echo its object’s field names/values as the old deploy and fall back to dotted strings. Bare literals (numbers, true, null) in a topics array are skipped without an answer on both sides of the window, unchanged.

Composite sources — a contract multiplier of exactly 1 is no longer a unit mismatch

What changed

  • agg_book/vol_book sources whose contractMultiplier is exactly 1 are now accepted. The rule was always about units: a source is refused when its quantities do not speak the base asset. A multiplier of one says one contract IS one unit of the base asset, so those quantities already are base units and nothing is being mixed. The check previously refused every stated multiplier, one included.
  • Everything else still answers unit_mismatch, unchanged: any multiplier other than one — 0.01, 100, whatever the venue states — is a genuine unit change, and conversion through the multiplier remains deferred.
  • No code, field, or frame shape changes: unit_mismatch stays in the error enum with the same meaning and the same message text, and an admitted source serves exactly like any other (scales echo, margins and MID normalisation apply, images render identically).

Who this unblocks: reference data states 1 on rows other venues leave unstated — every OKX spot instrument carries it, as do linear perpetuals on several venues. Those instruments could not be used as a composite source at all; now they can.

Deploy window: before the upgrade, a source with a stated multiplier of 1 answers unit_mismatch per topic and nothing arms; after, it validates and serves. Sources with a multiplier of one are the only behaviour that moves — 0 (unstated) sources and genuinely contract-sized sources behave identically on both sides, so a client whose subscriptions were succeeding before sees no change. A client that retried on unit_mismatch should expect those topics to start serving; one that treats unit_mismatch as permanent needs no change beyond re-subscribing to pick the instrument up.

Composite books, phase D — vol_book volume ladders

What’s new

  • The vol_book channel is served. Subscribe with an object entry (channel: "vol_book") carrying the same source vocabulary as agg_book (margins and MID normalisation included) plus the ladder: pricingModel (VWAP | BEST | WORST), bucketSizes (decimal strings at the resolved output qtyScale — positive, distinct by value, any order: the server sorts, the response ascends, and the optional positional bidAdjBps/askAdjBps pair with the sizes as submitted). Identity, replace, budgets, status frames, and reconnect behave exactly as agg_book; the session composite budget counts both channels together.
  • Frames are full ladder images (event: "vol_book"): ascending buckets, each present side an object of cum (the achieved cumulative quantity — the threshold when filled, the side’s whole depth when it exhausts, with filled: false and every deeper bucket sharing that tail), px (the model price), adjPx (with the bucket’s directional margin — bid down, ask up), and notional — the exact swept cost at priceScale + qtyScale. It can exceed a 64-bit integer: parse it as the arbitrary-precision decimal string it is. A side with no levels is absent (one-sided markets publish); spreadBps rides every bucket both sides price — partial fills included and the adjusted mid nonzero (a maximal bid margin can floor a sub-tick-scale price to an adjusted zero; the field is omitted rather than divided by zero) — over the adjusted mid at a fixed two-decimal scale, negative when independent venues cross. VWAP rounds to nearest with ties away from zero (an estimate, not a quote); BEST/WORST quote actual levels.
  • bad_ladder is answered. Present-but-wrong ladder values (an unknown model, a non-positive/duplicate/finer-than-scale size, more than composites.max.buckets buckets, an adj array that does not pair) answer bad_ladder per topic; an absent required field (pricingModel, bucketSizes) answers malformed, as does maxDepth on a vol_book entry (it is agg_book vocabulary — ladders walk as deep as their buckets need) or a ladder field on an agg_book entry.

Deploy window: before the upgrade, any vol_book entry answers unknown_channel per topic (nothing arms); after, it validates and serves. bad_ladder was already in the documented code enum (bind-once). One agg_book edge tightens: a ladder field attached to an agg_book entry was silently ignored before (the fields did not exist) and now answers malformed — a schema-valid frame is unaffected, and everything else behaves identically on both sides.

Composite books, phase C — MID normalisation

What’s new

  • normalisationMode: "MID" is served. A MID source (with the now-meaningful normalisedAsset) has its prices normalised through the internal index: the current (source-quote → target) rate multiplies each price before margins, rounding directionally (bids down, asks up — an adjustment never flatters a price). Rates are read fresh at every render, and a rate refresh itself marks the composite dirty exactly like a source-book tick — a standing image re-renders on the next tier tick when its rate moves, even while every source book is quiet.
  • fx_expired is emitted. A MID source whose pair has no current rate (absent, or older than the index’s 30 s consumer window) leaves the merge and the topic says so — status: "degraded" with reason: "fx_expired"; a returning rate announces recovered. Expiry is watched on the gateway’s health cadence (1 s by default), so the announcement lands within about a second even while every source book is quiet.
  • live now also requires FX: a composite acks live only when every enabled source book is synced and every required pair is current; otherwise syncing, kept by the first computable tick.
  • Validation: normalisedAsset is required with MID and must resolve to a known asset that differs from the composite’s base asset and from the source’s own quote asset — each violation answers bad_normalisation. The touch modes stay reserved and answer the same code.
  • Normalisation never changes a topic’s scale identity: output priceScale/qtyScale stay the max over the configured sources, exactly as phase B advertised them.

Deploy window: before the upgrade, any normalisationMode: "MID" entry answers bad_normalisation per topic (nothing arms); after, it validates and serves. Nothing else changes shape: fx_expired was already in the documented reason enum (bind-once), and NONE-mode composites behave identically on both sides.

Composite books, phase B — agg_book

What’s new

  • Object-form subscribe entries. params.topics now accepts objects beside strings. An object entry subscribes a client-configured composite: channel (a required const — agg_book), name (your handle, session-scoped, 1–64 of [A-Za-z0-9._-]), tier (required — composites are always conflated), optional maxDepth, and 1–16 sources (canonical symbols, optional per-side bidMarginBps/askMarginBps, optional disabled). All sources must share one base asset; contract-sized derivatives are rejected (unit_mismatch) in v1.
  • The composite ack row. live/syncing rows echo the resolved output priceScale/qtyScale — they bound every number the topic can carry (bind your parser’s precision once; a replace that only toggles disabled never changes them). Error rows carry a closed code (see the AsyncAPI enum) plus a human message you should not parse. live means every enabled source book is synced and the full image pushes immediately; syncing promises the first image at the first tick the composite becomes computable.
  • agg_book data frames are full images at each tier tick: idempotent, no chain semantics, both sides always present ([] when empty — one-sided markets publish). Levels are objects — px/qty at the output scales plus src per-venue attribution (venue-native raw prices at each venue’s own scales). ts is the newest included source’s exchangeTsNs; seq/seqTs are stream-position provenance, not a per-topic chain.
  • Degradation is explicit. A source whose book cannot be served leaves the merge and a status frame says so (status: "degraded", excluded: [{s, reason}]); recovery announces itself (status: "recovered"). All enabled sources out → one status: "stale" marker, then silence, then the healing image. The reason enum is complete in the contract (stale, fx_expired, delisted, disabled) so you bind once; this release serves stale — fx_expired arrives with FX normalisation.
  • Reconfigure with "replace": true on a live (name, tier): the swap is atomic (no unsubscribe, no gap, no syncing flash while sources stay synced), a failing replace answers its error code and leaves the old config serving, and an identical re-subscribe (your reconnect replay) is an idempotent re-ack. A different config without the flag answers name_in_use — reconfiguration is never an accident. The tier is topic identity: changing it is unsubscribe + resubscribe.
  • Unsubscribe by the composed topic string alone (agg_book.<name>.<tier>, byte-identical to the echo in acks and frames) — the config is never re-sent.
  • Budgets: at most 16 composites per session, 16 sources per composite, depth ≤ 50 (deployment-configurable). Over-budget subscribes answer budget_exceeded per topic.
  • Reserved, not yet served: normalisationMode: "MID" (+ normalisedAsset) answers bad_normalisation until MID normalisation is served (phase C, above); vol_book entries answer unknown_channel until volume ladders are (phase D).

Deploy window (a not-yet-upgraded gateway vs an upgraded one):

  • Do not mix object entries into frames an old gateway might serve. The old parser reads the topics array as loose quoted strings with no nesting awareness: an object entry comes back as a flood of spurious per-string error rows (its keys and values each read as a “topic”, so the ack can carry MORE rows than entries you sent, breaking order-correlation for that frame), and collection stops at the first ] the parser meets — the entry’s own sources array’s — so every entry after the object in the same frame, plain string topics included, is silently dropped: never subscribed, never answered. A client-chosen name that happens to look like a servable topic string could even be subscribed by the old parser. During the window: send composites in their own subscribe frame, after your string subscriptions, and treat “no ack row matching my composed agg_book.<name>.<tier> topic” as “old gateway — retry later”, never as an error.
  • On the upgraded gateway, string-topic behaviour is unchanged, including acks, all existing channels, and unsubscribe. The ack row’s new fields (priceScale/qtyScale/code/message) appear only on composite rows, so existing parsers are unaffected.
  • A bare-string subscribe of a composite topic ("agg_book.X.250ms" as a string entry) answers error on both sides of the window: the config is the subscription.