> For the complete documentation index, see [llms.txt](https://docs.hydromancer.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.hydromancer.xyz/readme/websocket/changelog.md).

# WebSocket changelog

Customer-visible WebSocket protocol changes

## 2026-09-09 — Address-scoped unsubscribe on user-entity channels

* `unsubscribe` with `addresses` (`users` for `userOrderUpdates`) now **subtracts** those addresses from the subscription the connection holds, leaving the rest streaming. Previously subscribing addresses merged them into one subscription but unsubscribing required the request to name the **entire** merged set — any smaller request was acknowledged and removed nothing, so those addresses kept delivering and their subscription units were never released. A connection that added and dropped addresses over time therefore climbed toward its tier's total-subscription limit and eventually got `Total subscription limit reached for tier ... (N/M subscriptions)` while holding far fewer addresses than N.
* Applies to `userFills`, `userOrderUpdates`, `userNonFundingLedgerEvents`, `userTwapStatusUpdates`, `userLeverageUpdates`, `userIsolatedMarginUpdates` and `userCompletedTrades`.
* Unsubscribing every address held drops the subscription and sets `unsubscribed: ["<channel>"]`, as before. Addresses you do not hold are ignored.
* The subscription's other parameters pick the merge group: `aggregateByTime` for `userFills`, `dex` and `addStakingEvents` for `userNonFundingLedgerEvents`, `dex` for `userCompletedTrades`. An unsubscribe subtracts only from the group whose parameters match, so two `userFills` subscriptions that differ on `aggregateByTime` stay independent.
* No wire-format change: no field was added, removed, or renamed.

## 2026-09-08 — hip4MarketEvents subscription

* New `hip4MarketEvents` subscription: every HIP-4 outcome and question lifecycle transition — registration, child association, description change and settlement — pushed as the chain produces it, replacing polling `registeredOutcomes` / `settledOutcomes` on a timer. Short-duration recurring markets can register and settle between polls; this channel does not miss them.
* Optional `venue`, `category`, `subCategory` and `class` filters, matched case-insensitively and combined with AND. Omit all of them to receive every HIP-4 market event. An event that does not carry a filtered field never matches that filter — validator-registered markets carry no `venue`, price markets carry no `category`, canonical markets carry no `class`.
* Each event carries `entity` (`outcome` or `question`), `action`, and `time` / `blockNumber` / `txIndex`, so every step is timestamped. Only executed actions are delivered.
* Question events carry `namedOutcomes` (the child outcomes of a question) and, on settlement, `settleWinningOutcomeId` and `childSettles`.
* Reconnect replay: subscribe with your last `cursor` to replay missed events; the same filters apply to replay as to live.
* Venue lifecycle (a venue activating or deactivating) is **not** on this channel — use the REST `outcomeVenuesOverview`.
* Additive: no existing channel or field changes.

## 2026-09-02 — perpPrices subscription

* New `perpPrices` subscription: the accepted oracle/mark/external perp prices of the subscribed `coins` on every dex including HIP-3 — the streaming counterpart of `perpPriceHistoryByTime`, with row-for-row identical fields.
* A message is sent for **every block**, `prices: []` when the block carried no round for your coins, so the chain height and time can be tracked continuously and the per-connection `seq` is gap-free.
* Coins always merge: a repeat subscribe unions into the connection's single `perpPrices` subscription, `unsubscribe` with `coins` subtracts, without `coins` it removes the subscription.
* Reconnect replay: subscribe with your last `cursor` to replay up to 30 seconds of missed rounds (tick messages are not replayed); `hasGap` flags a cursor older than the cache.
* Additive: no existing channel or field changes.

## 2026-08-28 — Subscription ack ordering

* A subscription change is now acknowledged **after** every frame that was already in flight when it committed. Previously an `l4BookUpdates` `merge: true` subscribe could be acked with the merged coin list while the next frame — a block already being fanned out — still covered only the previous set, which read as a coin intermittently going missing right after it was added.
* Applies to `l4BookUpdates`, `userFills` / `builderFills` / `allFills` / `liquidationFills` / `trades`, `userOrderUpdates` / `builderOrderUpdates` / `tpslUpdates`, `userNonFundingLedgerEvents` / `allUserNonFundingLedgerEvents` / `fundingRates`, the TWAP status channels, and the leverage / isolated-margin channels. `l2Book`, `l2BookDiff`, `bbo`, the candle, asset-context, mids, and dashboard channels are unchanged for now — their acks may still precede an in-flight frame.
* The rule to code against: each frame carries the coin set its block was fanned out with, and the ack marks the boundary. Frames before the ack may predate the change; every frame after it reflects it. Apply a coin-set change on the ack, not on the request. The hold is bounded — if fan-out stalls past it the ack is released early, and every block still being fanned out at that moment can deliver a pre-change frame after it (blocks fan out concurrently, so this is not limited to one frame). It resolves once those blocks drain.
* Same rule for `unsubscribe`: a trailing frame containing dropped coins now arrives **before** the ack, not after it (this replaces the note in the 2026-08-20 entry below).
* No wire-format change: no field was added, removed, or renamed.

## 2026-08-20 — Opt-in merge for l4BookUpdates

* Coin-list `l4BookUpdates` stays one stream per subscribe by default. `{"type":"l4BookUpdates","coins":["ETH"]}` then `{"type":"l4BookUpdates","coins":["BTC"]}` is still two frames per block.
* Set `"merge": true` on a coin-list subscribe to union those coins into the connection's existing coin-list slots. The next block is one frame covering the merged set, so adding an instrument does not need an unsubscribe/resubscribe cycle. The subscribe confirmation echoes the merged coin list. `merge` is subscribe-only: it is rejected on an `unsubscribe`, on the all-markets firehose, and on other subscription types.
* `unsubscribe` with `coins` **subtracts** when the connection holds a single coin-list slot (one batched subscribe, or the result of `merge: true`). Several coin-list slots keep exact-match unsubscribe. Coins you do not hold are ignored. `unsubscribe` without `coins` still removes the all-markets firehose only. A coin-scoped unsubscribe against only the firehose is rejected.
* The tier coin cap now applies to the **effective** (post-merge) coin set rather than to each raw request, so repeated `merge: true` (and the existing `trades` / `allFills` unions) cannot walk past it. A single request is still capped at 200 names.
* When a subscribe carries a `cursor`, replay covers the coins named in **that request**. After a merge the ack still echoes the full merged list.
* After a coin-scoped `unsubscribe` is acknowledged, one already-in-flight block may still deliver a frame containing the dropped coins. Steady state resumes from the next block.
* Draining the coin-list while the all-markets firehose is still held does **not** set `unsubscribed: ["l4BookUpdates"]` — that field means the channel is gone, and the firehose is still on it.
* **Upcoming:** `merge` will become the **default** for coin-list `l4BookUpdates` in a future release. Set it explicitly now — `"merge": true` to adopt early, `"merge": false` to pin today's one-stream-per-subscribe behaviour through the change. The flag stays accepted afterwards; only clients that send neither will see a behaviour change.

## 2026-08-07 — Trigger TWAP fields and userTwapStatusUpdates

* New `userTwapStatusUpdates` subscription: TWAP status events filtered to the subscribed `addresses` (multi-address, merge-on-resubscribe, cursor replay supported) — the user-scoped counterpart of `allTwapStatusUpdates`.
* TWAP status updates (both channels) now carry the trigger-TWAP fields the node added on 2026-07-25: `triggerPx`, `triggerAbove`, and `stopPx`. The fields are omitted for plain TWAPs.
* New TWAP `status` values from the same node update: `waitingForTrigger` and `stopped`.

## 2026-08-04 — requestId bounds and outcome market activity

* Optional `requestId` is capped at 128 characters and must not contain control characters. Violations reject with `code: "invalid_request_id"` (oversized values are not echoed).
* Inbound WebSocket frames are capped (default 256 KiB, `WS_MAX_INBOUND_MESSAGE_BYTES`) so control-plane messages cannot use Axum’s 64 MiB default.
* Active-market metadata refresh now includes HIP-4 outcome markets via `outcomeMeta`, so outcome wire coins (`#{outcome_id * 10 + side_index}`) receive the same active/inactive advisories as perps and spot. If `outcomeMeta` fails, the server still commits the perp+spot snapshot (`partial_success`) and treats outcome activity as unavailable until the next full refresh.

## 2026-07-22 — Attributable subscription feedback

* Subscribe and unsubscribe requests may include an optional string `requestId`; confirmations and rejects echo it.
* `subscriptionUpdate` retains `subscribed` and `failed` and adds `operation`, `subscription`, and, for unsubscribe confirmations, `unsubscribed`.
* Subscription errors retain `type` and `message` and add request context. Orderbook-family invalid-coin and coin-count rejects also include stable `code` and `details` fields.
* `l2Book`, `l2BookDiff`, `l4BookUpdates`, and `bbo` confirmations may include advisory `warnings` for inactive coins or unavailable activity metadata. These warnings do not fail the subscription.
* Orderbook invalid-name errors now name every distinct invalid value. Requests containing more than 200 coin entries now report a coin-count error instead of an address-format error.

These changes are additive except for the corrected error text. Clients should continue reading the existing fields they need and ignore unknown fields and message types.
