Dev log: real on-chain trading volume, and the bug that hid it
A short write-up of a feature we just shipped — real 24h trading volume for every Robinhood Chain stock token — including the serverless timeout that quietly kept the numbers at zero, and the fix.
Until now, a token's 24h volumefield was always empty — we had a price feed and supply, but no volume source. It's now live: every token page shows real USD swap volume over the last day, broken out per poolso you can see exactly where the flow is, plus a total. The same numbers flow into the API's /defi endpoint and, per candle, into /ohlc — turning OHLC into true OHLCV for backtesting.
Every tracked pool is a STOCK/USDG pair, and USDG is a USD-pegged stablecoin (≈ $1, 6 decimals). That makes valuation clean: for each Uniswap V3 Swap event, the absolute value of its USDG leg isthe trade's USD volume — no external price oracle needed.
An indexer walks each pool's Swap logs, buckets them into hourly USD totals, and keeps a per-pool block cursor so every run only reads new swaps — nothing is double-counted. A rollup then sums the last 24 hours into the volume_24h each pool exposes. Hourly granularity is deliberate: it aggregates cleanly into 1h / 4h / 1d OHLC candles too.
First deploy, and the numbers didn't show. The odd part: the hourly table had 38 rows covering a full 26-hour backfill, ~$33k of volume indexed correctly — but every pool's volume_24h was still null. The indexer clearly worked. So what didn't?
The job row told the story: status stuck at running, with no finish time. The first-run backfill — every pool, up to dozens of paginated log requests each — ran past the serverless function's 60-second maxDuration and was killed mid-loop. And the step that refreshes volume_24h runs after the loop. So the hourly rows got written, the function died before the rollup, and — because the killed run never released its lock — the next runs skipped for ten minutes. The data was all there; the one query that surfaces it never got to run.
Two changes. First, the run is now time-bounded: with a longer ceiling (300s on our plan) and an internal wall-clock budget, the indexing loop stops cleanly between pools — and stops paginating a single busy pool — well before the platform's hard kill. Each pool advances its own cursor, so the next run just continues.
Second, and the actual root cause: the volume_24h rollup now always runs, even on a partial pass. A time-limited run is normal, not an error — it refreshes volume from whatever hourly data exists so far and fills in over the next few 15-minute ticks. The lesson worth keeping: when a long job has a “publish the result” step, that step can't live behind an unbounded loop that a timeout can cut off.
- Token pages — total + per-pool 24h volume in the Liquidity card.
- GET /api/agent/defi/{symbol} — volume24hUsd per Uniswap V3 pool.
- GET /api/agent/ohlc/{symbol} — every candle now carries volumeUsd and swapCount.
- SDKs — typed access in the official TypeScript and Python clients, and via the MCP get_ohlc tool.
One convention worth calling out: a bucket with no indexed volume reads null, not 0. “We don't have data yet” and “genuinely no trades” are different facts, and we keep them distinct everywhere.
Alongside the feature: the API docs picked up per-candle volume in the OHLC schema and refreshed code samples, and we added a Concepts glossary that defines every metric on a token page — price source, uiMultiplier and corporate-action adjusted supply, TVL, Morpho APY, USDG, and now volume. We also chased down a small but persistent log warning: the database URL carried a sslacceptparam the runtime driver didn't recognize, printed on every connect — stripped now, TLS unchanged. Small things, but a clean log is worth keeping clean.