Lot of stuff landed. All of this is either live in dev or rolling right now.
Decoded it — it's ftPositionManagerMinEquity(), the protocol's per-position equity floor. 5x on $0.10 gives you $0.10 of equity backing $0.40 of borrow, which is below what ftDNMM will let you open. That's why the order failed.
Added a stable code for it: ORDER_BELOW_MIN_EQUITY. Shows up on the POST response (code) and the poll response (errorCode). Friendly message: "position equity below PM minimum (bump up sellAmount or lower leverage)". Keep equity ≥ $1 on dev and you won't hit it.
I wired the indexer to proxy to the executor's GET /leverage/orders. Failed orders show up in the Order History table with the revert reason:
{
"status": "failed",
"errorMessage": "ORDER_REVERTED: Error(string): Return amount is not enough",
"errorCode": "ORDER_REVERTED"
}So you see every order — pending, filled, failed — not just fills.
Was hardcoded "0". Now returns notional / leverage in USD. Falls back to "0" only if mark or leverage aren't available — honest zero, not a fabricated number.
Shorts with liquidationPriceReason="user_debt_unavailable" (on-chain debt cleared) and longs with "user_collateral_unavailable" are filtered out. No more ghost rows from positions that already got closed out.
rateis a plain numeric string ("0.0009"). No more%suffix — you can append it at render time.total=0for fresh users (count was joining on exposures only, so a parked USDC balance blew it up to 72).sizescaled by base decimals (no more132458992489623049wS values).
Old rows (from before the scaling fix shipped) with raw-wei price or fee auto-salvage at read time:
- Price: when stored price is > 100× the current mark, substitute
notional / mark. - Fee: when
fee > notional * 10, divide by decimals to get the human value.
New rows index correctly from the start. No DB migration, fix is pure read-path.
Fires the instant POST /mm/leverage/orders accepts the order — BEFORE the tx even goes on-chain. You get the frame ~50-100ms after the POST returns.
{
"type": "order_pending",
"address": "0x…",
"chainId": 146,
"data": {
"orderId": "lev_…",
"status": "pending",
"orderType": "open|close|swap",
"createdAt": 1776944355,
"invalidate": ["order"]
}
}Dispatch on type like the other account-channel frames. Insert the row into Open Orders immediately; the order_filled or order_failed frame updates it in place via orderId when the tx resolves.
You said you'd rather not refetch REST after every balance_update. Fair. Every frame now embeds data.balances: AccountBalance[] — exact same shape as GET /mm/spot/account/balance:
{
"type": "balance_update",
"address": "0x…",
"chainId": 146,
"data": {
"asset": "0x…", "eventSig": "Borrow", "blockNumber": 12345,
"timestamp": 1776944355, "invalidate": true,
"balances": [
{ "chainId": 146, "network": "Sonic",
"token": "wS", "tokenAddress": "0x…",
"totalBalance": "1.5", "usdValue": "0.065",
"pnlPct": "+2.4%" }
]
}
}Replace your balances state directly from the frame. Skip the REST call entirely.
Graceful degrade: if the snapshot build times out (>2s), the frame falls back to signal-only (no balances field). Your hook should handle both — when balances present, replace state; when absent, treat as invalidation. Same handler either way.
This is the big one you pushed for. The indexer now subscribes to the chain's WebSocket newHeads stream and runs the collector the instant a new head lands. Before: 2-4s floor (chain block + 1s collector poll + 1s projector poll). Now: block time + WS hop, roughly 100-300ms to balance_update arrival.
Pure-poll fallback kicks in automatically if the WS endpoint drops — the collector degrades in-place without a process restart. Dev is using the Alchemy wss endpoint.
Combined with the rich balance_update above, a Withdraw should land in your table within ~500ms end-to-end (block + WS + projector + fan-out + render). Not 3-4s anymore.
Correction: session flow is already 0 wallet popups per order. The ephemeral delegate key signs SessionCall locally in useSessionManager.signSessionCall. If you're seeing 3 popups on TP + SL, it means delegateKeyRef.current is empty when signSessionCall fires and the fallback walletClient.signTypedData path kicks in — that's 1 popup per order because the wallet's actually signing. Check localStorage for the key session_<addr>_dk — if it disappears mid-flow you've got a session-restore bug, not a signature-count one.
The real nonce issue is separate: executor keys idempotency on (sessionId, nonce). While Order A is pending at nonce N, submitting Order B with the same nonce just dedupes back to A. That breaks OCO (TP + SL pending together) and limit-stacking.
Two fixes, I can ship the second one this week if you want it:
- Contract-side: non-sequential nonces. Bitmap of used nonces within a sliding window (say 64 slots). Clean, but needs a redeploy.
- Executor-side OCO. Relax idempotency: allow multiple pending orders at the same
(sessionId, nonce)when they're tagged mutually-exclusive. When one fires, the session nonce advances → the others auto-invalidate → executor marks themcancelled. That's literally OCO for free. Flag it on submit withocoGroupIdand we're done. ~100 lines, no contract work.
Keep sigType: "session" for everything. Do NOT route limits to EIP-712 — that would actually add popups.
| Item | Status | What I need |
|---|---|---|
/account/summary aggregate |
Not shipped | Confirm the field list (perpBalance, crossMarginRatio, maintenanceMargin, unrealizedPnl, crossAccountLeverage) and I'll ship |
| OCO / nonce lock | See #10 above | Say go and I'll do option 2 |
| SL trigger-price watcher | Not shipped | Priority? It's its own little service |
| Relayer PM.approveBorrow / approveEngine | Out of my lane | SC + relayer team call |
| Dev oracle price staleness | Not a code bug | Operator thing — dev oracle router isn't being pushed fresh prices |
If anything above looks wrong after dev settles, send me the exact request (URL + response for REST, or raw frame for WS) and I'll trace it. The rich balance_update + order_pending shapes are new — tell me the moment they don't look right and I'll fix same day.