Skip to main content

Monaco Protocol SDK v1.0.61

This release covers the consumer-facing changes since v1.0.60. A perp market now publishes its full notional-bracket margin ladder on riskTiers instead of the single synthetic tier the field used to carry, and the same ladder prices read-time margin and liquidation across the position, portfolio and margin-account endpoints — so anything that read riskTiers[0] as “the” tier, or derived a requirement from the top-level ratios alone, now needs to select a bracket. No field was removed or renamed, but the array’s length and meaning both change and a caller that does not migrate reads tier-1 limits for a higher-notional position without any error — so it is listed as breaking. The order-risk simulations return expectedMatchResult — the engine’s own simulated fill for the previewed size, in the same MatchResult shape a placement carries — plus referencePrice, and accept an optional slippageToleranceBps so the preview runs under the band the real order will. And a margin reduce-only replace may now name a close size at or below what the order has already filled, which was refused on every order before. If you read riskTiers, carry a previewed slippage tolerance into placement, or resize partially filled reduce-only closes, read the three upgrade notes before you bump.

Breaking

riskTiers carries the full margin ladder

GET /api/v1/market/pairs/{trading_pair_id}/perp/config (sdk.market.getPerpMarketConfig; gRPC MarketService.GetPerpMarketConfig, public, no auth) now lists every bracket in riskTiers, ascending by lowerBoundNotional. The field previously reported a single synthetic tier, so its length and its meaning both change. Each RiskTier gains five fields: The deductions keep the ladder continuous where one bracket hands over to the next. For a position of notional N in bracket k:
Each bracket’s rates are marginal, so reading a bracket’s initialMarginRatio as a flat rate against the whole notional overstates that bracket’s ladder floor at the bottom of the bracket. It is not the position’s initial margin in either direction: that is the greater of the floor and the selected-leverage requirement below, which at low leverage is far above the flat-rate figure. A position’s bracket is the last tier whose lowerBoundNotional is at or below its notional, abs(quantity) x markPrice. Its actual initial margin is the greater of IM_floor(N) and the selected-leverage initial margin scaled to the current mark — the ladder sets a floor, it does not replace the leverage you chose. The top-level maxLeverage, initialMarginRatio and maintenanceMarginRatio are unchanged and remain the tier-1 values, so a caller reading only those reads the lowest bracket rather than the position’s own. maxPositionSize stays a market-wide maximum absolute base-asset quantity repeated on every tier, and its deprecated maxPositionNotional alias is unchanged — it is a base quantity, not a notional. The same ladder now prices maintenance margin and liquidation prices on the position, portfolio and margin-account reads, so a position whose notional sits in a higher bracket reports a higher maintenance requirement and a closer liquidation price than the flat tier-1 rate would have given. Positions inside tier 1 are unaffected: its deductions are "0", which leaves the arithmetic exactly as before. See Margin ladders for the derivation and getPerpMarketConfig for the field reference.

Added

Order-risk previews return the engine’s simulated fill

sdk.marginAccounts.simulateOrderRisk (POST /api/v1/margin/accounts/{margin_account_id}/simulate-order-risk), simulateParentMarginOrderRisk (POST /api/v1/margin/parent-margin-account/simulate-order-risk) and simulateRiskBucketOrderRisk (POST /api/v1/margin/risk-buckets/simulate-order-risk) — and their gRPC equivalents MarginAccountsService.SimulateOrderRisk, SimulateParentMarginOrderRisk and SimulateRiskBucketOrderRisk — now publish the match the engine already ran to admit the order. All three need an authenticated session and are owner-scoped: a preview only ever runs against the caller’s own margin accounts. SimulateOrderRiskResponse.expectedMatchResult?: MatchResult is the same shape CreateOrderResponse.matchResult carries for a placement (and ReplaceOrderResponse.matchResult, BatchCreateResult.matchResult, BatchReplaceResult.matchResult; the Order read model does not carry it), from the same walk over the live book: totalFilled, remainingQuantity, averageFillPrice, executionPriceRange, status, actualSlippageBps and maxSlippageBps. It is not a client-side re-walk, so a preflight check and the placement that follows read the same numbers for the same book — but the book moves, so treat the figures as indicative of the size’s impact rather than as a guaranteed execution. remainingQuantity is the partial-fill indicator — it is what the band or the depth would leave unfilled on the previewed book, and the preview’s status reads CANCELLED for it, because a MARKET remainder cannot rest. Placement runs against a later book, so a non-zero remainder here is a warning that the size does not clear, not a prediction of the fill: read the placement’s own status and quantities for what actually happened. SimulateOrderRiskResponse.referencePrice?: string is the touch on the taking side the MARKET preview was measured from — best ask for a BUY, best bid for a SELL — and is what actualSlippageBps is measured against. Both fields are present only when accepted is true, and absent — never zeros — on every refused preview, including the post-match maker-risk rejection. Treat absence as unknown rather than as a zero fill. referencePrice is additionally MARKET-only: a LIMIT or IOC preview is measured against its own limit price and publishes nothing here, and its actualSlippageBps is null when every fill improved on that price. The three requests gain an optional slippageToleranceBps (MARKET only, 01000), validated client-side by the newly exported MarketSlippageToleranceBpsSchema: a LIMIT preview carrying the field, or a value above 1,000 bps, fails validation before the request is sent, and reaches the server as a 400 (gRPC INVALID_ARGUMENT) carrying placement’s own message if sent raw. It tightens the previewed walk to the band the real order will run under, so expectedMatchResult and estimatedFee price under your own tolerance; the public handler previously always previewed under the default band. It can only tighten — it never widens the 1,000 bps protective band. The preview also now runs under the caller’s wallet rather than a placeholder, so self-trade prevention classifies your own resting orders exactly as placement does. These figures price against the book at the moment of the preview. A take-profit or stop-loss closes later, on a different book and under the wider band a triggered market leg carries, anchored at trigger time — so size a TP/SL target by applying expectedMatchResult.actualSlippageBps to your trigger price as an impact figure, rather than displaying this call’s absolute averageFillPrice. A preview cannot carry a post-only flag, TP/SL legs or a per-order self-trade-prevention override, so a post-only order that would cross is refused by placement, not by the preview. On the MCP server, simulate_parent_margin_order_risk and simulate_risk_bucket_order_risk accept the same input and surface both response fields. The MCP server ships at 1.0.61 too; it runs via npx -y @0xmonaco/mcp-server, so restarting your assistant picks up the new tool schema. See Pre-trade simulation.

Fixed

A margin reduce-only replace can be sized down below its filled amount

sdk.trading.replaceOrder (PUT /api/v1/orders/{orderId}) and each item of sdk.trading.batchReplace (POST /api/v1/orders/batch-replace; gRPC OrdersService.ReplaceOrder and BatchReplaceOrders) rejected quantity at or below the order’s filledQuantity on every order, with New quantity N must be greater than filled quantity 3. That rule belongs to total semantics: a replacement quantity is the order’s new total, and a total at or below what already filled leaves nothing to rest. A margin reduce-only quantity is deliberately not a total — it is a close size measured against the live position and placed exactly as given — so the rule never applied to it. Applying it anyway made a partially filled close unshrinkable: on a reduce-only close of 8 with 3 filled, both quantity: "2" and quantity: "3" were refused, leaving no way to reprice the close downward except by naming a size above the order’s own fill count. Both are now accepted and placed at the requested close size. As with any replacement, success does not mean the order is resting: a reduce-only close still goes through matching and self-trade prevention, so it can fill immediately, or be cancelled outright under CANCEL_TAKER / CANCEL_BOTH or as a SKIP remainder crossing one of your surviving orders — read the new order’s status rather than inferring it. The replacement still inherits the original’s fill history, so the row reads quantity = filled + remaining. Nothing else changes. Total semantics, the total - filled subtraction and the must-exceed-filled rejection all behave exactly as before for every order that is not margin reduce-only, and the reduce-only position bound still rejects a close larger than the live position. No request or response shape changed — this is a server-side correction, so clients on any SDK version receive it; only the quantity field descriptions on ReplaceOrderRequest and BatchReplaceOrderItem now state the exception. See Replacing Orders.

Upgrade

Three things to check before upgrading. If you read riskTiers, stop treating it as one row: select the last tier whose lowerBoundNotional is at or below abs(quantity) x markPrice, take the position’s initial margin as the greater of that bracket’s ladder floor and your selected-leverage requirement, and handle maxLeverage being absent on a single-tier market. The top-level ratios are tier 1, not the position’s tier. If you carry a previewed slippage tolerance into placement, note the units differ: previews take slippageToleranceBps as integer basis points, while placeMarketOrder and the batch helpers take slippageTolerance as a ratio, so pass slippageToleranceBps / 10_000 — a slippageToleranceBps key on those calls is not forwarded. And if you resize partially filled reduce-only closes, the 400 you were coding around is gone; a close size at or below the fill count is now a legitimate size-down, so drop any client-side floor that was compensating for it. The rest is additive. expectedMatchResult and referencePrice are present only on an accepted preview, so read them behind an accepted check rather than assuming a shape. Existing preview calls that omit slippageToleranceBps keep the default band and are unchanged, except that the walk now runs under your own wallet for self-trade prevention.