Monaco Protocol SDK v1.0.69
This release rolls up the consumer-facing changes shipped since v1.0.67.sdk.ws.conditionalOrders() now receives exactly one versioned update per transition, TWAP children get orders frames tagged with their parentOrderId and the resting orders they fill get their maker fills, trade fees report the total fee paid split into monacoFee + builderFee, and BuilderCodes payouts name their payout stream. Closed positions no longer count a partial-close fee twice, and a position closed over many fills reports its lifetime figures. A risk bucket entering liquidation has its resting orders cancelled at once, a first-use automatically funded SELL limit order is no longer refused for want of margin it does not need, the faucet quota is scoped to each wallet and application, and public whitelist submissions are closed. Action items: if you consume conditional_order_update, handle the new position_closed, reparented and expired reasons and merge by version; if you read fee from user trades or orders fill frames, it now includes the builder fee — read monacoFee for Monaco’s share; if you call sdk.whitelist.submit, it now fails with 403; and if you use the Rust gRPC SDK, ListBuildercodePayoutsRequest no longer derives Copy.
Changed
One versioned conditional-order update per transition
sdk.ws.conditionalOrders() receives exactly one conditional_order_update for every conditional-order transition, including OCO sibling cancels (oco_cancelled), cancels when the position closes (position_closed) or the entry order ends (parent_cancelled), activation of a pending leg (activated), failed and expired triggers (expired), and legs carried onto a replaced entry order (reparented). ConditionalOrderEventReason gains position_closed, reparented and expired. Every live update carries version, the revision of the row it describes — the same value REST and the subscribe-time snapshot return — so merge state from any of the three with incoming.version >= local.version when both carry one; a versioned row supersedes an unversioned one. The triggered update carries state TRIGGERED with triggeredOrderId and triggeredAt. Created and cancelled updates are sent once the change is durable, slightly later than before. See WebSockets.
Trade fees report the total fee paid
sdk.profile.getUserTrades() (GET /api/v1/accounts/trades; gRPC AccountsService.GetUserTrades, and TradesService.ListUserTrades) returns UserTrade.fee as the total fee the user paid on the trade: for a taker, Monaco’s taker fee plus the frontend’s builder fee (applicationTakerFee); for a maker, the maker fee (negative for a rebate). Previously a taker’s fee left out the builder fee. Two optional components sit beside it, so fee = monacoFee + builderFee: monacoFee (Monaco’s share) and builderFee (always "0" for a maker, because builder fees are charged to takers only). The accounts UserTrade also gains monacoFeeRaw and builderFeeRaw.
orders fill frames (OrderFilled, OrderPartiallyFilled, OrderMatched) carry fee as the total fee for the fills they cover and add monacoFee / monacoFeeRaw and builderFee / builderFeeRaw (wire monaco_fee, monaco_fee_raw, builder_fee, builder_fee_raw). A taker frame’s fee now includes the builder fee and matches the order echo’s totalTakerFees; a maker frame’s builderFee is "0". The components are optional because a server older than them omits them, and orders snapshot rows carry no fee fields. No value changes today: every application charges a 0 bps builder fee, so builderFee is "0" and fee equals monacoFee on every trade.
BuilderCodes payouts carry their payout stream
EveryBuildercodePayout row from sdk.buildercodes.listPayouts() (GET /api/v1/buildercodes/payouts; gRPC BuildercodeRewardsService.ListBuildercodePayouts) gains kind: REVENUE_SHARE (the frontend’s share of Monaco’s fee) or BUILDER_FEE (the frontend’s own builder fee, paid by the taker). Both credit the same claimable balance; every credit today is REVENUE_SHARE, because the builder fee a taker pays is not yet credited to the payout bucket. listPayouts({ kind }) filters to one stream — the SDK rejects any other value before a request is sent, and the server returns 400 (INVALID_ARGUMENT on gRPC) for one — and total / totalPages then count that stream only; with no filter the listing is unchanged. sdk.buildercodes.getPayoutSummary() (GET /api/v1/buildercodes/payouts/summary; gRPC GetBuildercodePayoutSummary) keeps byToken as before, summed across both streams, and adds byTokenAndKind: one row per token and stream with token, kind, totalAmount (RAW), payoutCount and lastCreditedAt, and a token’s rows add up to its byToken total. @0xmonaco/types adds BuildercodePayoutKind and BuildercodePayoutTokenKindTotal with their schemas; BuildercodePayout.kind and GetBuildercodePayoutSummaryResponse.byTokenAndKind are optional because an older server omits them. In the Rust gRPC SDK, ListBuildercodePayoutsRequest no longer derives Copy because it now holds a String — clone it where code relied on implicit copies. See BuilderCodes.
Public whitelist submissions are closed
sdk.whitelist.submit (POST /api/v1/whitelist; gRPC WhitelistService.SubmitWhitelist) rejects every decoded request with 403 FORBIDDEN (PERMISSION_DENIED on gRPC): "Public whitelist applications are closed." The request fields are unchanged for client compatibility, and the legacy success response stays in the generated types but is no longer returned. Existing applicant records and administrative approvals are preserved, and a submission reads or writes no applicant data.
Faucet quota is per wallet and application
The faucet’s daily limit applies to each wallet on each application it signs in to, rather than to the wallet across all applications.sdk.faucet.mint (POST /api/v1/faucet/mint; gRPC FaucetService.MintTokens) enforces it per wallet and application, and sdk.faucet.getInfo (GET /api/v1/faucet/info; gRPC FaucetService.GetFaucetInfo) counts and lists only the calling application’s requests. No wire shape changes. See Faucet.
Added
orders frames for TWAP children and the makers they fill
A child order a TWAP parent places is an ordinary order on the orders channel: a taker slice sends OrderPlaced, its taker fill and, when the price band stops it short, an OrderCancelled for the remainder; a passive child sends OrderPlaced when it rests, a maker fill each time it trades, and OrderCancelled when it leaves the book (terminalReason REPLACED on a reprice or deadline sweep, USER_REQUESTED when the parent is cancelled or ends). Each resting order a TWAP slice fills receives its maker fill as it would from any other taker. Previously none of these frames were sent. Every TWAP-child frame and orders snapshot row carries parentOrderId, the same id getOrder returns; it is absent for any other order. CommonOrderEventData and OrderSnapshotItem gain optional parentOrderId, and the REST Order type now declares it. A resting order that a TWAP slice cancels — for self-trade prevention, or because it failed its risk re-check during the match — receives no OrderCancelled frame, so read its terminal state from REST. See WebSockets.
Fixed
Resting orders are cancelled when a risk bucket enters liquidation
When a risk bucket breaches maintenance margin and entersPendingLiquidation, every one of its resting orders is cancelled, starting in the entry that records the transition — up to 150 there, with any remainder in consecutive entries sequenced immediately after it and nothing interleaved — whether the breach came from an oracle mark, a funding settlement, an auto-deleveraging haircut or a fill — and each sends an OrderCancelled frame with terminalReason LIQUIDATION. Previously those orders stayed on the book until the liquidation itself reached the bucket, and in that window another bucket’s liquidation could fill them as makers and deepen the breach. See Liquidations.
First-use automatically funded sells are sized from their limit
An automatically fundedLIMIT order on a new risk bucket is sized from its submitted limit price. When a SELL executes above that limit, admission tops the bucket up from the parent margin account, bounded by the parent’s capacity, instead of leaving it short of initial margin. Previously the sizing read the public best bid, which could include the wallet’s own orders that self-trade prevention would cancel, so otherwise affordable orders were refused. This applies to ISOLATED and CROSS risk buckets, and order-risk previews use the same admission path. Explicit riskBucketCollateral stays fee-capped, and unused fee funding is still returned only on ISOLATED buckets. See Trade.
Closed positions no longer count a partial-close fee twice
GET /api/v1/positions and GET /api/v1/positions/{position_id} (gRPC PositionsService.ListPositions, GetPosition) no longer count a partial-close fee twice. With complete fee history, optional cumFees is the position’s whole-life fee total — opening and reducing fees, application fees, liquidation fees and signed rebates — and a closed position’s netRealizedPnl and realizedRoe deduct it once. Existing historical attribution errors are not repaired, and a closed position whose fee history is no longer retained keeps its closing-execution netRealizedPnl and realizedRoe. feesPaid remains closing-only and exitPrice remains the execution VWAP across all closes. No ledger charges change. See Positions.
Positions closed over many fills report lifetime figures
A closed position is served from its reducing executions whenever those executions reconcile against its storedrealizedPnl to within one raw quote unit (0.000001). The check used to compare them exactly, so ordinary rounding noise pushed healthy positions onto the stored fallback. Affected positions now populate exitPrice, netRealizedPnl, realizedRoe, fundingPaid and feesPaid where they came back null, and size and isolatedMargin carry the lifetime closed quantity and allocated margin basis rather than the pre-close remainder. realizedPnl is unchanged. Because this is a read-path change, already-closed positions report the corrected values immediately.
Upgrade
ListBuildercodePayoutsRequest no longer derives Copy, so code that reuses a moved request must clone() it. Four behaviours differ: ConditionalOrderEventReason has three more values, so an exhaustive switch needs cases for them; fee on user trades and orders fill frames includes the builder fee; sdk.whitelist.submit always fails with 403; and the faucet quota is counted per application.
