API
The read model behind the desk: REST endpoints and a WebSocket over indexed chain state. It serves the same figures at every hour, because the desk never closes, and everything it serves can be re-derived from the chain.
What it is
The api is a Fastify service that serves one snapshot of chain state, refreshed on the tick cadence (TICK_MS, five seconds by default) rather than read from the chain per request, and indexes contract events, prices, NAV and TVL snapshots into a local database for history. It is a pure read model: it signs nothing, holds no keys with write power, and every state transition it reports happened on chain first. The base URL depends on the deployment; the web app reads it from its NEXT_PUBLIC_API_URL environment variable, and every path below is relative to that host.
Units
Every numeric chain value crosses the wire as a decimal string to avoid JavaScript number precision loss. Money fields are USDG with 6 decimal places, prices carry 8 decimal places, per-second index values are WAD (18 decimal places), and rates are integer basis points. Convert on the client; the api never sends floating point.
REST endpoints
| Endpoint | Description |
|---|---|
/health | The health decision: 200 with the network and the snapshot, chain and index counters when the api is serving current truth, 503 with a count of reasons when the snapshot is stale, the chain unreachable, the event index stalled or a critical alert is active inside its cooldown. The public monitoring routes share one rule: they say how many alerts and how bad, never which. So this body carries the active alert count by level and a reason count, never a key or a reason's text, because which enforcement is not running is for the operator console alone. Under version it names the build answering: the commit sha, the release tag and the release time the service was built with, each null on a build made outside the release path. |
/health?liveness=1 | The restart probe: fails on a stale snapshot or an unreachable chain alone, so a hosting platform restarts the process for the faults a restart can fix and not for a paused chain or a standing alert. |
/api/alerts | The last 200 alerts the api raised through the shared alert sink, as counts: the active set and the buffer each counted by level (total, critical, warn), and the sink's delivery counters. No alert appears in the body in any reduction, not its key, its service, its times, its sentence or its details, under the same rule as /health: the key names the enforcement that is not running and the details are where an account appears, and both are for the operator console alone. The same alerts are posted to ALERT_WEBHOOK_URL when one is set. The keeper's alerts arrive by post through /api/alerts/ingest when the two are configured for it and are counted here under the keeper's name; the alerts themselves are read on the operations console's session route, /api/admin/alerts, which answers 401 without a session. |
/api/meta | Deployment snapshot: network, chain id, contract and token addresses, per-asset margin configuration, and headline rate and vault parameters. The frontend boots from this and never reads deployment files itself. |
/api/state | Global state: interest index and rates, vault NAV, utilisation, insurance, skim state and deposit cap, current prices and per-asset halt flags. There is no session or calendar block: the desk is open continuously, so there is no open or closed flag to serve. |
/api/account/:address | Full margin picture for one account: collateral value, debit, equity, initial and maintenance requirements, account state, positions valued at the mark (each with pendingQty, the part sold in a clip of today's charge and settling: included in qty and in the figures, but not sellable until the apply page), per-asset buying power, call state (in call, raised at, deadline), frozen flag and staking discount. Memoised for five seconds per address; a poll inside that window is served from the memo and not charged. Budgeted per IP together with the vault, staking and points views: past 240 chain reads in a minute a request the memo cannot answer gets 429 until the minute has passed. ?fresh=1 skips the memo and is honoured inside that budget; past it the memo is served. |
/api/account/:address/activity | The most recent 50 indexed contract events touching the account, newest first, with block, tx hash and decoded arguments. Each row carries a final flag that turns true once 64 blocks have built above it; a row the chain reorganises away before then is withdrawn on the indexer's next poll. |
/api/vault/:address | Cash vault position for one depositor: shares, value at current NAV, and any pending withdrawal request with its claimable time. Memoised and budgeted per IP the way the account view is. |
/api/prices/:symbol/history?hours= | Price series for one asset, 8 decimal places, downsampled to at most 48 points. hours defaults to 24 and is capped at 45 days. The three history series share one per-IP budget: past 240 calls in a minute they answer 429 until the minute has passed. |
/api/tvl-history?hours= | Two series in 6dp USDG: vault TVL and desk collateral custody, downsampled to at most 200 points each. |
/api/vault-nav-history?hours= | Trailing vault NAV series (WAD values), used by the UI for the 30 day NAV change. |
/api/protocol | Protocol revenue in its two legs: every final InterestSettled event and every final TradeFeeCharged event with its tx hash, cumulative totals over those rows (the skim to protocol and the desk fees, with the fee count), the block they are final to, and the live unswept protocol balance awaiting the sweep to the foundation treasury. A settlement is served once 64 blocks have built above it, never from the bare chain head. |
/api/stocks | Robinhood Chain mainnet stock token tracker, independent of the chain the desk runs on, flagged with which tickers the desk lists. |
/api/staking/:address | Staking state for one account plus the desk-wide picture: staked $CALL, tier and discount, accrued reward, the pending unstake, wallet $CALL balance, and the chain-read tier table with the reserve figures. Serves available: false with a reason in three postures: a deployment with no staking contract, the deferred posture a real deployment launches in (the desk opened before $CALL was issued, so every account pays the base rate until the contract is wired in after the token launch), and a pending activation whose two governor operations have not executed yet, naming when they mature. |
/api/points/:address | Points and referral state for one account: interest points, vault points, the season multiplier, the bound referrer, accounts referred and the referral bonus. Served from the event index; returns 503 until the index has reached the chain head, because a partial sum would understate. |
/api/referral | POST. Binds a referral once: the body carries the account, the referrer code and a signature over the exact referral message, which names the chain id and the MarginAccounts address this endpoint serves in /api/meta, so a signature made for any other deployment, one sharing this network name included, binds nothing here. The signature is verified against the account before anything is stored; an account cannot refer itself or rebind. Budgeted per IP: past 5 requests in a minute it answers 429 until the minute has passed. |
/api/venue-quote?symbol&side¬ional|qty&slippageBps | Previews a venue fill against the oracle mark: mark price, fill price, the gap in basis points (positive when worse for the trader, on a buy and on a sale alike), the quoted output in raw units (amountOut) and the basis of the figure. Buys are sized by notional in 6dp USDG; sells take a qty in token units, or a notional the api converts at the mark. exact is true when the figure is the fill the venue would deliver now: a v2 pool at its reserves (basis "pool reserves"), the mock venue ("venue quote"), or the concentrated venue simulated against the pool by an eth_call state override ("pool simulation"). When that simulation cannot answer, the adapter's at-the-price figure is served with exact false and basis "adapter estimate". depthExceeded is true, with a null fill, when the pool cannot take the whole order. Returns a null fill when the asset is halted or the venue has no liquidity. slippageBps is the tolerance the floor is derived at, in basis points: omitted or empty it is the desk's default of 100 (the figure the order ticket starts at), a value you send is yours, zero included, and anything over the ceiling the deployed Router enforces (200 bps at the parameter file's value; /api/meta serves the deployed figure as routing.userSlippageCeilingBps) is held to that ceiling; the response repeats the figure it used as slippageBps. deskMinOut is the desk's own minimum out at that tolerance, exactly as the Router computes it (the fair quantity at the mark on a buy, the value at the sale reference on a sale, less the tolerance): the floor the chain enforces on the fill, whichever path fills. On a deployment carrying the routed venue path the response also carries route, the Uniswap Trading API's route for exactly this order (its commands, inputs and deadline as the chain takes them, expectedOut and the API's own apiMinimumOut, gapBps against the mark, meetsFloor true when expectedOut clears deskMinOut, hooked, routeString, gasUseEstimate, pools and fetchedAt), or null, and routeBasis says which: "trading api" when a route is served, "unavailable: rate limited", "unavailable: no key" or "unavailable: no route" when none is, and "not on this deployment" where no routed path exists. Budgeted per IP: past 60 calls in a minute it answers 429 until the minute has passed. |
A shortened account response, the shape the desk overview is built from:
GET /api/account/0xf39F...2266
{
"address": "0xf39F...2266",
"cv": "25000000000",
"debit": "12000181000",
"equity": "12999819000",
"im": "10000000000",
"mm": "6250000000",
"state": 0,
"excessEquity": "2999819000",
"maintenanceExcess": "6749819000",
"usdgBalance": "0",
"principalScaled": "11987421305672108440000",
"principalCash": "12000000000",
"accruedInterest": "181000",
"frozen": false,
"call": { "inCall": false, "raisedAt": "0", "deadline": "0" },
"positions": [
{ "symbol": "AAPL", "qty": "50000000000000000000", "value": "11500000000", "pendingQty": "0", "class": 5 }
],
"buyingPower": { "AAPL": "5999638000", "WETH": "8570911428" },
"stakingDiscountBps": "0"
}The WebSocket
Connect to /ws on the same host and the api pushes a tick every few seconds (five by default, set by TICK_MS): the current interest index, the gross rate, the vault NAV, every asset price, and chainTs, the latest block timestamp in seconds. Deadlines the contracts hold, a withdrawal or unstake claimableAt, are judged against that chain clock, never the wall clock.
{
"type": "tick",
"ts": 1756742400000,
"chainTs": 1756742399,
"indexWad": "1001507893218472011",
"grossRateBps": "1250",
"navWad": "1000214000000000000",
"prices": { "AAPL": "23000000000", "WETH": "300000000000" }
}The tick does not carry your debit. The account endpoint serves principalScaled together with the account's own accrual terms, its tier index and tier rate (the gross rate for an unstaked account), and the client computes the ticking figure locally, advancing that index between pushes at that rate per second:
That is why the debit on screen moves every second while the socket only ticks every few: the account read re-anchors the index, the client extrapolates in between. The same rule is in the UI spec, section 9.
Nothing the api serves is authoritative. Addresses come from the deployment the api loads, figures come from contract reads, activity and revenue come from indexed events that carry their tx hashes. The indexer keeps the hash of every block it scanned and re-reads the hash at its head before each extension, so a chain reorganisation rolls the index back to the last block both sides agree on and rescans from there. Any claim on any page can be checked against the chain directly, and the api has no ability to move funds, change parameters or act for an account.