Monaco Protocol SDK v1.0.80
This release rolls up the consumer-facing changes shipped since v1.0.77. Positions report whether their lifetime totals are complete, closed and liquidated position lists sort newest-closed first, and a flat margin account now gets anaccount frame when its collateral moves. Perp instruments frames carry the whole margin ladder, the trades channel sends the newest 50 trades on subscribe, risk-bucket rows report maxRemovableCollateral, and the Rust gRPC SDK gains an optional serde feature. Action items: restart a status=CLOSED or status=LIQUIDATED position walk with an empty pageToken; read lifetimeTotalsStatus before trusting a position’s lifetime totals, and never read an absent total as zero; handle timeInForce: null on MARKET order events; parse insufficient-balance amounts as asset units, not raw integers.
Added
lifetimeTotalsStatus on positions
sdk.positions.listPositions (GET /api/v1/positions; gRPC PositionsService.ListPositions) and sdk.positions.getPosition (GET /api/v1/positions/{positionId}; gRPC PositionsService.GetPosition) return lifetimeTotalsStatus on every position. It says whether the lifetime fields (exitPrice, realizedRoe, feesPaid, fundingPaid, cumFees, netRealizedPnl) can be trusted:
LIFETIME_TOTALS_STATUS_COMPLETE— the position history covers the whole lifetime, and the lifetime fields follow their existing rules.LIFETIME_TOTALS_STATUS_PENDING— history is still being written, or the row changed while it was being read. Every lifetime field is absent. Retry the read after a short backoff (seconds).LIFETIME_TOTALS_STATUS_INCOMPLETE— the totals are not complete now, and a quick retry will not change that. A gap in legacy history is permanent; a missing or drifted totals row is repaired by a background reconcile, after which the position readsCOMPLETE.
exitPrice can be present while cumFees is absent. Never read an absent total as zero. OPEN and LIQUIDATING positions are now reconciled against their realizedPnl like closed ones: one whose history does not account for it reports INCOMPLETE and no longer returns cumFees, which it used to return understated. Closed, liquidated and expired positions return the same lifetime fields as before. WebSocket position frames do not carry the field. @0xmonaco/types adds the LifetimeTotalsStatus type, usePositions exposes the field, the MCP get_position / get_positions tools return it, and the Rust REST and gRPC SDKs gain lifetime_totals_status. See Positions.
The margin ladder on instruments frames
sdk.ws.instruments frames for a perp market carry riskTiers, the whole margin ladder ascending by tierLevel, with the same fields as the REST perp config’s riskTiers, on both the subscribe-time snapshot and live frames. Any margin-ladder edit now sends a config_change frame whose changedFields names risk_tiers (plus max_leverage, initial_margin_rate and maintenance_margin_rate when tier 1 changed). Spot frames omit the field, and a frame from an older server arrives with riskTiers undefined. See WebSockets.
A subscribe-time snapshot on trades
sdk.ws.trades(tradingPairId, handler, onSnapshot?) takes an optional onSnapshot that receives the market’s newest 50 trades, newest first, when you subscribe. It is a recent-history baseline, not a gap-free replay: it can overlap the first live trades or miss one executed just before you subscribed, so dedupe on tradeId. See WebSockets.
maxRemovableCollateral on risk-bucket rows
Risk-bucket rows of sdk.marginAccounts.getMarginAccountSummary and getParentMarginAccountSummary (MarginAccountSummary, GetMarginAccountSummaryResponse) and of sdk.portfolio.getMargin (PortfolioRiskBucket) carry maxRemovableCollateral: the largest removal the risk engine accepts when it may also lower the posted margin of an isolated bucket’s one open position, down to its initial margin at the mark. The part past withdrawableCollateral comes out of the position’s margin, raising its leverage and moving its liquidation price toward the mark. It equals withdrawableCollateral on a cross bucket and on an isolated one with no open position or several, and is present only on live-engine reads. withdrawableCollateral is unchanged. See Margin Accounts.
Optional serde feature in the Rust gRPC SDK
monaco-grpc-sdk gains an optional serde feature. With it on, every generated message and enum derives Serialize / Deserialize in the JSON shape the REST API uses: camelCase field names, the uint64 lease and quote fences as decimal strings, unset optional fields omitted where REST omits them, and the REST snake_case aliases accepted. It is off by default and changes nothing on the gRPC wire. See Rust gRPC SDK.
Changed
Closed and liquidated positions list newest-closed first
sdk.positions.listPositions (GET /api/v1/positions; gRPC PositionsService.ListPositions) with status=CLOSED or status=LIQUIDATED now orders by (closedAt, positionId) descending, so a long-held position closed today is on the first page. Every other filter keeps its order. A status=CLOSED or status=LIQUIDATED pageToken issued before this release is rejected with 400; restart that walk with an empty pageToken. A position closed from this release on takes the instant the matching engine emitted the close as its close time, and a terminal position’s updatedAt reports it. No request or response fields changed.
Flat margin accounts get account frames
sdk.ws.account now sends a live account_update frame for a flat margin account (no open position and no resting order) after its collateral moves: a transfer between the spot wallet and margin, an on-chain deposit into margin or a withdrawal from it, a transfer between your margin accounts, closing a sub-account, a copy-trading allocation, or an insurance payout. Moves that land close together collapse into one frame carrying the latest state, normally within about 2 seconds. The frame uses the account’s existing sequence, so the dedupe rule is unchanged. A flat account still sends no periodic keepalive. No wire-shape change.
Fixed
- Market orders report no time in force — every WebSocket order event of a MARKET order carries
timeInForce: null, andOrder.timeInForceis absent (nullon the orders snapshot), including for existing market orders. Before,OrderPlacedsaidGTC, and engine-placed market orders such as TWAP slices and copy orders saidIOC. LIMIT orders keep their time in force. - Win rate counts fees — the
winners,losersandwinRatePctofsdk.portfolio.getRealizedPnland the copy-trading leaderwinRatePctclassify a perp close by its realized PnL net of fees. Before, a close that was profitable before fees but lost money after them counted as a winner. Closes counted before this release are not reclassified. - Cross transfers into a closed risk bucket move the money — a transfer into a cross risk bucket that had been emptied and closed now reopens it and moves the collateral. Before, it could report success without moving anything.
- Isolated order previews work after the bucket closes —
sdk.marginAccounts.simulateRiskBucketOrderRiskon a pair whose isolated risk bucket was closed now previews like a first isolated order, instead of returning a permanent “still being provisioned”400. - Insufficient-balance errors show asset units — a wallet shortfall on a spot order, a margin collateral transfer-in or a withdrawal reports
AvailableandRequiredin the asset’s units (Required: 5000, notRequired: 5000000000). A transfer or withdrawal above the account’s whole balance now returns the insufficient-balance error instead of an isolated-collateral message. - A risk bucket leaves bad debt flat — once the insurance fund covers a bucket’s shortfall, an isolated risk bucket closes and a cross risk bucket returns to normal, empty. See Liquidations.
- Referral and BuilderCodes reads are per client application — the docs now say that
getMyReferralPosition().uplinelists the levels your trades on this client application pay, and can be empty whilereferreris set;getMyReferralTree()goes as deep as any client application rewards (at most 10); and the BuilderCodes config fields describe this application’s values. No wire change.
Upgrade
pageToken. Check lifetimeTotalsStatus before using a position’s lifetime totals.
