Skip to content

Instantly share code, notes, and snippets.

@patcito
Created April 16, 2026 18:38
Show Gist options
  • Select an option

  • Save patcito/bd1cc3249a574b8925c976061315627d to your computer and use it in GitHub Desktop.

Select an option

Save patcito/bd1cc3249a574b8925c976061315627d to your computer and use it in GitHub Desktop.
Spot API additions — HF, partial/full liquidation prices, on-chain enrichment

Spot API additions — health factor, liquidation prices, on-chain enrichment

Addendum to the main spec. These are new/populated fields on existing endpoints.

Base URL (dev): https://api.opbalance.com/mm/spot


GET /account/positions?address=<>&chainId=<> — 3 new fields

Every position row now includes:

{
  // ... existing fields ...
  healthFactor: string            // account-wide, equity/maint. "999999" when no debt.
  partialLiquidationPrice: string // price at which HF < target → soft seizure begins
  fullLiquidationPrice: string    // price at which HF = 1.0 → full closeout (Regime B)
  liquidationPrice: string        // legacy alias = partialLiquidationPrice
}

What the numbers mean

ftDNMM has three liquidation regimes (from /home/pe/newnewrepos/w/flyingtulip/ftDNMM):

Regime HF range What happens
A HF ≥ target (e.g. 1.5) Safe. No liquidation possible.
B 1.0 ≤ HF < target Soft liquidation. Liquidator seizes just enough to restore HF to target.
C HF < 1.0 Forced full closeout. All collateral seized.
  • partialLiquidationPrice = price at which you enter Regime B (warning — start of soft seizure)
  • fullLiquidationPrice = price at which you enter Regime C (full closeout)
  • For a long position: partialLiquidationPrice > fullLiquidationPrice (partial triggers earlier as price falls)
  • For a short position: inverted — partial triggers earlier as price rises

UI suggestion

Show both:

Liq. price: $38,450 (partial) / $32,100 (full)
HF: 1.52

Or a single price with colored warning:

⚠ Soft liq at $38,450 | 💥 Full liq at $32,100

Legacy liquidationPrice keeps the partial value so nothing breaks if you're already using it.

Fallbacks

  • All three return "0" if the on-chain call reverts (log-warned server-side, never 500).
  • healthFactor returns "999999" when maintUSDWad == 0 (user has collateral but no debt). Treat as infinity.

GET /markets?chainId=<> — now populated from on-chain

Previously the following were placeholder "0". Now live for spot rows:

{
  symbol: "WS-USDC",
  marketType: "spot",
  marketCap: "10874864.5551",   // totalSupply × oracle price / 10^decimals
  openInterest: "30.0317",       // LendingLens.astate.borrows × price
  fundingRate: "57.6597%",       // IRM.borrowAPR(asset, util)
  priceChange24h: "+0.0020",     // from 24h candle window
  priceChangePct24h: "+4.5677%", // same
  volume24h: "0",                // still zero (no trade volume tracking yet — TODO)
}

Perps twin rows still emit marketCap: "0" because perps have no supply. Other fields populate for both.


GET /account/balance?address=<>&chainId=<>usdValue now populated

{
  token: "WS",
  totalBalance: "1234.5678",
  usdValue: "55.28",   // balance × oracle.priceUSD(token) / 10^decimals / 1e18
  pnlPct: "0%",        // still placeholder
}

Falls back to ticker markPrice if oracle reverts, then to "0" if both fail.


Caching

Server-side TTL cache (transparent to client):

  • Oracle prices: 5s
  • All other on-chain reads (assetCfg, accountValues, etc.): 30s
  • Failures cached briefly (~5s) to avoid hammering RPC on reverts
  • Per-key mutex coalesces concurrent readers — no thundering herd

Still placeholder (protocol limits, not lazy)

Field Why
volume24h We don't track trade events as volume yet. TODO.
pnlPct (balance) Would require a cost-basis tracker per user.
marginType ftDNMM is cross-margin only — hardcoded "cross" is correct.
TWAP endpoints No on-chain TWAP infra. Empty array until off-chain keeper is built.
POST /order / DELETE /order/:id 501 — executor owns wallet-signed writes. Stays that way.

Env (for reference)

Indexer reads SPOT_ORACLE_ROUTER_ADDR and SPOT_LENDING_LENSE_ADDR from ECS env. Both are set on dev. If either is missing, enrichment falls back to placeholders (graceful).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment