All 15 items are now done in dev (items marked 🟡 need you to verify end-to-end).
Dev endpoints: REST https://api.opbalance.com/mm/spot/*, WS same host / /spot/ws.
Fee was base units for all sides; now scaled by the sell-token's decimals so it matches the fee-currency you render. Also fee-currency tracks the side (sell → base, buy → quote).
Verify: /spot/trades/:market_key → fee is human-scaled for both sides.
formatFundingRatePct was dividing the cumulative rate_index instead of the per-event delta, so you saw values like 1000000%. Now uses e.Accrued (per-event delta) divided by WAD, with the first event getting a zero baseline.
Verify: /spot/funding/:asset → ratePct reads like "0.0042%" not scientific notation.
Revert decoder now walks nested map/DataError shapes (previously only string-asserted ErrorData). Added reasonCodeBySig so each known selector maps to a stable FE code (ORDER_TOO_SMALL, SESSION_CAP_EXCEEDED, …). extractErrorCode recognises the ORDER_* / SESSION_CAP_* prefixes on poll responses.
Verify: submit an order that should fail (sellAmount=1 wei) → response {success:false, code:"ORDER_TOO_SMALL", error:"…"}. Poll /leverage/orders/:id → same errorCode after on-chain revert.
Perps twin row in /markets-v2 now carries maxLeverage: 10 (defaultPerpMaxLeverage). Spot rows omit the field (json:"maxLeverage,omitempty").
Follow-up: wire per-asset maxLeverageBps from ftLendingLens later; 10 is a conservative default for now.
Verify: /spot/markets-v2 → every perps row has maxLeverage: 10, spot rows don't.
Added leverage string field on Position. Value comes from on-chain LendingLens.accountValues.PnlAdjCollUSDWad / EquityUSDWad — the same formula the codebase already uses for /account/balance.
Rendered as an integer string ("2", "5") matching the slider. Returns "1" when there's no debt or equity is ≤ 0 (underwater).
Verify: open a 3x leveraged order in dev, hit /spot/positions/:addr → leverage: "3".
Root cause: MM refresh loop continues past any token whose LendingLense.AssetState(token) reverts, so assets not yet enabled on-chain silently dropped out of Redis. Fix merges the static config universe into the response — any LendingTokens entry missing from Redis now synthesises a row with collateralEnabled: false, ltvBps: 0. Price + balance still work for those rows.
Verify: GET /mm/spot/leverage/assets?chainId=146 → wBTC + wETH rows present. FE can gray out entries with collateralEnabled: false if you want a "pending activation" cue.
These frames arrive via the indexer's /internal/account-event webhook. The executor wasn't posting to it. Shipped:
- New
internal/notifierin leverage-executor — bounded fire-and-forget HTTP client with 64-slot semaphore, 2s timeout, body-drain-to-EOF, startup healthcheck. - After every fill, the executor POSTs one frame (see next section). No more two-POSTs-per-fill race.
- Infra wired both sides' secrets + DNS in dev (
opbalance-dev).
Channel: subscribe to account:<addr>:c146 (per-chain) or account:<addr> (chain-agnostic). Auth is still the client-supplied address field — the real signed-challenge is on the infra backlog.
Previous design sent order_update + perp_account_update as two independent POSTs — under an indexer hiccup they could land partially. Now one POST per terminal status with an invalidate array telling you which caches to refresh:
{
"type": "order_filled",
"address": "0x…",
"chainId": 146,
"data": {
"orderId": "…",
"status": "filled",
"txHash": "0x…",
"orderType": "open",
"filledAt": 1735689600,
"invalidate": ["order", "positions", "balances"]
}
}On failure:
{
"type": "order_failed",
"address": "0x…",
"chainId": 146,
"data": {
"orderId": "…",
"status": "failed",
"txHash": "0x…",
"orderType": "open",
"error": "ORDER_TOO_SMALL: …",
"invalidate": ["order"]
}
}Action for you: dispatch on type. For order_filled refresh order row + positions + balances; for order_failed refresh order row and show the toast. Don't listen for order_update / perp_account_update anymore — those names are retired on the executor side.
🟡 Verify after dev CI rolls the infra task def (~minutes after this gist lands).
Added a trades publisher in the indexer — fires only on rows that actually committed (ON CONFLICT DO NOTHING replays don't re-broadcast). Frame shape mirrors api.Trade so you reuse the REST schema.
Channel: trades:<SYMBOL>:c146 (untyped) / :mspot / :mperp. Plus legacy trades:<market_key>.
Verify: subscribe, submit a leverage order in dev → you should see a frame within ~1-2 blocks.
Was emitting flat top-level fields (asset, eventSig, blockNumber, …). Now wrapped to match the /internal/account-event envelope:
{
"type": "balance_update",
"address": "0x…",
"chainId": 146,
"data": {
"asset": "0x…",
"eventSig": "Deposit",
"blockNumber": 12345,
"timestamp": 1735689600,
"invalidate": true
}
}All three account-channel frame types (order_filled, order_failed, balance_update) share this envelope — one dispatch on type covers all.
Indexer now fans real 24h stats into each ticker publish frame. Volume comes from trades.notional (oracle candles always have 0 volume, that was the bug). Price change uses the latest vs the candle closest to latest.bucket_start - 24h, with the same source tie-breaker (mark > dex > other) REST already uses. Stats loaded once at the top of each tick so all markets share the same snapshot.
Verify: subscribe to tickers:WBTC-USDC:c146 → priceChange24h, priceChangePct24h, volume24h are non-zero after a few minutes of trades.
Candle poller only persisted/published on bucket boundary or high-low shift, so flat-price ticks dropped the klines stream. Now every same-bucket tick emits a WS delta (no DB write) so your chart keeps a heartbeat.
Verify: subscribe to klines:WBTC-USDC:c146:1m on a quiet market — you should see a type:"delta" frame each second even when OHLC hasn't changed.
# 1. Markets list has maxLeverage on perps
curl -s 'https://api.opbalance.com/mm/spot/markets-v2?chainId=146' | jq '.data[] | select(.marketType=="perps") | {marketKey,maxLeverage}' | head
# 2. Leverage assets list has WBTC + WETH
curl -s 'https://api.opbalance.com/mm/spot/leverage/assets?chainId=146' | jq '[.data[].symbol]'
# 3. Position leverage non-1 (replace <addr>)
curl -s 'https://api.opbalance.com/mm/spot/positions/<addr>' | jq '.data.positions[] | {marketKey, leverage}'
# 4. Ticker WS snapshot
wscat -c 'wss://api.opbalance.com/spot/ws' \
-x '{"op":"subscribe","channels":[{"name":"tickers","symbol":"WBTC-USDC","chainId":146}]}'
# 5. Account channel (see order_filled / balance_update live)
wscat -c 'wss://api.opbalance.com/spot/ws' \
-x '{"op":"subscribe","address":"<your addr>","channels":[{"name":"account","chainIds":[146]}]}'Ping if any surface still shows the old shape after the infra rollout settles — I'll loop back.