Skip to main content

Monaco Protocol SDK v1.0.47

This release is mostly TWAP. Parents gain a second execution style — PASSIVE, which rests each slice as a post-only maker quote at the touch and takes only what the window forces — and a conditional mode that keeps a parent dormant until the market reaches a price you name. Alongside those, a postOnly batch-replace item that would cross the book no longer costs you the original order, funding payments now appear in the movements feed, and the account WebSocket channel carries a tie-breaking sequence. Two things deserve reading before you upgrade: the corrected clientOrderId guidance, which previously told you to do something unsafe, and the exact scope of the batch-replace change.

Added

PASSIVE TWAP execution style

sdk.trading.createTwapOrder() accepts an optional executionStyle of "TAKER" or "PASSIVE". TAKER is the default and is the existing v1 behavior, unchanged — banded MARKET slices fired at each slot. PASSIVE works each slice as a maker instead. The engine rests a post-only LIMIT GTC child at the same-side touch, chases the touch with throttled cancel/replace reprices, and at the slot deadline sweeps whatever the child did not earn as a banded MARKET child. The result is maker where the book allows it and taker only for what the window forces. Two bounds hold while chasing: the parent’s own limitPrice, which is never crossed, and the parent’s slippageToleranceBps band around the protective reference price — the oracle mark on a margin market when one is available, otherwise a robust book-derived venue price (from the best bid/ask, when the spread is not degenerate) falling back to the last trade. Spot parents always use that book-derived chain, since spot has no oracle mark. There is one exception worth knowing if you lean on the band for protection: when the engine can resolve no reference at all — a one-sided book on a pair that has not traded in this process, so no mark, no trustworthy two-sided venue price and no last trade — the band check is skipped rather than failing closed, and limitPrice becomes the only bound on the resting quote. Set one on a passive parent if you need a hard ceiling in a market that thin. A passive placement whose target price would cross executes immediately as a banded taker instead — the book is already offering at or better than the passive target, so resting would be pointless. Passive parents plan on their own cadence: a 60-second target slice interval by default, against the taker default of ~5 seconds, because a child has to rest long enough to earn queue position. The 2,880-slice cap is the same for both styles, so a long window stretches its interval rather than adding slices. Both the cadence and the reprice throttle (1 second minimum between attempts) are operator-tunable. Cancelling a passive parent is cancel-only and atomic: a resting child is removed in the same step that terminalizes the parent, fills it already earned stand, and nothing is left on the book. There is no final sweep on a user cancel. Fees follow each fill’s own side. Passive maker fills carry the pair’s maker fee and no application fee — the application fee is a taker-side fee — while the deadline sweeps and any crossing take pay application taker fees as usual. executionStyle is on the TWAP read model from getTwapOrder() / listTwapOrders(), and the twap_orders WebSocket channel gains three reasons: passive_child_placed (a post-only child now rests at the touch), child_repriced (the reprice step ran), and passive_fill (maker fills on the resting child folded into the parent). Deadline sweeps and crossing takes still surface as slice_placed. Two of those three carry a case worth reading before you build on them. child_repriced is also what the cancel-only fallback emits when the old child was cancelled but the repost did not happen — so it does not imply a child is resting. The channel gives you no way to tell the two apart either: TwapOrderEventData carries no active-child field, so resting-child status is simply not observable from twap_orders. Track the child through the ordinary order channels if you need it. And the maker fill that completes the parent reports completed, not passive_fill, so a client waiting on a final passive_fill waits forever. See TWAP Orders and Trading.

Conditional (price-triggered) TWAP parents

A TWAP parent can now wait for a price instead of only for a clock. Pass triggerPrice together with an explicit triggerDirection ("ABOVE" or "BELOW") — both or neither — and the parent is created PENDING, watching rather than executing, and activates when the reference price crosses your level. triggeredAt records when it fired, and activation is one-way: once trading, the trigger is never re-evaluated. The window floats. A conditional parent takes durationSeconds only — startTime and endTime are rejected — because its window is [trigger instant, trigger instant + durationSeconds] and cannot be known at creation. Read the armed window back off the parent rather than scheduling on the create response, whose firstSliceAt and endTime are provisional placeholders until the trigger fires. Optional conditionalExpiry bounds the wait: an RFC 3339 UTC deadline (Z suffix only) that is valid only alongside a trigger, must be in the future, and both defaults to and is capped at 30 days from creation. If the trigger has not fired by then the parent goes CANCELLED having executed nothing, with the terminal reason trigger expired. It fires on the clock alone, so a halted or unpriceable market cannot leave a parent waiting past its deadline. Two refusals to know about. A trigger already satisfied at creation returns 400 rather than starting immediately — omit triggerPrice if immediate execution is what you want. And on a margin market the reference is the oracle mark, so a create is rejected 400 while the market has no mark yet; an already-waiting parent keeps evaluating against the venue reference price if the mark later goes away. triggerPrice, triggerDirection, conditionalExpiry, and triggeredAt are all on the read model and on the twap_orders WebSocket payload, and the channel gains the triggered and trigger_expired reasons. See TWAP Orders.

sequence on the account WebSocket channel

Frames on the authenticated account channel now carry sequence, a monotonic per-margin-account publish counter within one producer run. It is a tie-breaker, not a stream cursor: dedupe by updatedAt with newest winning, and reach for sequence only to order two frames carrying the same updatedAt. It may skip values, and it resets to zero when the producer restarts — which can happen while your socket stays connected — so it is never a gap detector and never a cross-restart ordering mechanism. Every frame still carries the account’s full current health, so a skipped value costs you nothing. Note that the producer emitting these frames is still gated off by default and not enabled in the deployed environments, so this is a contract addition to build against rather than a live stream change. See WebSockets.

Changed

A crossing postOnly batch-replace item no longer costs you the original

batchReplace used to run its cancel pass first, so a postOnly replacement that would cross the book was refused and left you without the order it was replacing. The crossing check now runs in the pre-cancel preflight, so a refused item normally leaves its original resting — same order ID, same queue position. The item still reports error.code === "POST_ONLY_WOULD_CROSS"; only the fate of the original changed. This brings batchReplace into line with single replaceOrder, which already validated before cancelling. The check is evaluated against the book as it will stand once the batch’s own cancels are applied, so a two-sided ladder shifting across its own resting prices is unaffected. It is not a guarantee, because the preflight cannot see everything. Three narrow cases still report POST_ONLY_WOULD_CROSS with the original cancelled: your item crosses a replacement that an earlier item in the same batch just rested; it crosses another item’s original that failed to cancel and stayed on the book; or you listed the same original twice with different replacements, where a refused occurrence’s sibling still cancels the shared original. A fourth goes the other way — an earlier non-postOnly item that would have taken the touch is not visible either, so a quote that would have rested is refused and its original simply stays put. Confirm against your open orders rather than treating the error code alone as a guarantee. Unchanged: every other per-item placement failure — insufficient balance, a self-trade against an order outside the batch, a risk rejection — still leaves the original cancelled. See Batch Replace.

Funding payments appear in the movements feed

GET /api/v1/accounts/movements (sdk.profile.getPaginatedUserMovements()) now includes funding payments as funding transactions, unioned in at read time from the funding ledger. Filter to them with transactionType=FUNDING: the SDK method validates the value against an upper-case-only Zod enum and throws client-side on a lower-case one, while raw REST also accepts funding since the server matches case-insensitively. The amount is a magnitude, with entryType carrying the direction: DEBIT when the account paid funding, CREDIT when it received it. Like every other REST movement row, both enum values come back upper-case. A funding movement is a collateral USD delta rather than a token transfer, so it reports decimals of 0 and an amountRaw equal to its amount. It carries the same id as its live movements WebSocket frame, so a client consuming both feeds deduplicates by id — but the two payloads are not identical, so do not diff them field for field. The REST row keeps the upper-case enum values and resolves assetId, symbol, and decimals; the WebSocket frame uses the lower-case forms and carries no resolved asset id. Zero-amount funding windows are excluded, and funding is scoped to the requesting application like every other movement. See Account Balances.

MonacoWebSocket and MessageHandler now live in @0xmonaco/types

Both are re-exported from @0xmonaco/core rather than defined there, making @0xmonaco/types the single source of truth for the channel surface. The public surface is unchanged and existing imports from @0xmonaco/core keep working.

Fixed

MonacoSDK.ws types the full channel surface

createMonacoSDK() instances carried a stale inline copy of the WebSocket type, so positions, liquidations, account, instruments, marketStats, and marketStatsAll were invisible on sdk.ws even though the runtime had them, and ws.ohlcv did not accept its optional priceType argument. MonacoSDK.ws is now typed as the full MonacoWebSocket interface, so all six channels and the priceType argument type-check. Runtime behavior is unchanged — this only unblocks calls TypeScript was rejecting.

Corrected clientOrderId guidance — never retry on a lost response

The clientOrderId documentation was wrong in two ways, one of them dangerous. It described the handle as “not de-duplicated and not unique”, and it told callers to reconcile a lost response against order history before resubmitting. Uniqueness is enforced, but only among your orders that are resting on the book. An order that never rests — MARKET, IOC/FOK, or a LIMIT that fills completely on arrival — never claims the handle, so no conflict can be raised for it and a resubmission is accepted as a new order that can execute again. The handle is also released on any terminal state. It is a correlation handle, not an idempotency key, and it cannot make a lost-response retry safe. Reconciliation belongs on the order WebSocket stream, not on order history. The stream carries the handle even for orders that never rested, and a matching event there is positive proof the order landed — while silence is never evidence that it did not. Order history is replica-backed and can omit an order that did land, and it never contains an order rejected before acceptance at all. The same correction landed on the MCP place_limit_order, place_market_order, and replace_order tool descriptions. MCP exposes no order-stream tool, so an agent cannot resolve an ambiguous outcome from inside MCP: report it rather than retrying. No wire, schema, or behavior change — descriptions and documentation only.

Upgrade

The MCP server ships at 1.0.47 too; it runs via npx -y @0xmonaco/mcp-server, so restarting your assistant picks up the new version. Every SDK bump is a patch release. Nothing here is breaking: the TWAP additions are optional fields on an existing create, sequence and the funding movements are additive, and the batch-replace change only makes a rejection less costly. Two action items, though. First, if any code path retries an order after a lost or ambiguous response, keyed on clientOrderId for safety, change it — the handle does not protect a MARKET, IOC/FOK, or fully-filling LIMIT order, and a retry can execute twice. Reconcile on the order stream instead. Second, if you handle POST_ONLY_WOULD_CROSS in a batch-replace ladder by re-placing the original from scratch, that will now double it in the common case: re-price rather than re-place, and confirm against your open orders for the narrow cases that still cancel.