← Blog

2026-08-11

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.

What shipped
24h volume, measured on-chain

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.

How it works
Uniswap V3 Swap events, valued by the USDG leg

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.

The bug
38 rows of data, and a column full of null

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.

The fix
A wall-clock budget, and always finish the rollup

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.

Where to use it
Every surface, not just the site
  • 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.

Also
Docs, glossary, and a quieter log

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.

Browse tokens · Concepts · API docs · More posts