Every market data feed has holes in it. What separates them is what they put in the hole. A vendor that hands you a zero where it has no measurement has not given you data, it has given you a number that will survive every sanity check you write and be wrong. Here is what we do instead, rule by rule, with the mechanism named so you can check it.
Absent is not empty. “We did not measure this” and “we measured this and it was nothing” are completely different claims about the world, and a feed that renders them identically has destroyed the difference before your code ever sees it. Every rule below is that one rule applied somewhere specific.
{ "gamma": 0, "oi": 0, "pct": 0 }{ "gamma": null, "oi": null, "pct": null, "n": 3 }The ninety-coin cross-section ranks coins on one measure, on one clock. A coin we are missing a reading for is left out of the ranking, not padded with a zero and sorted to the bottom. That distinction is the whole product: a padded zero sorts the coins we know least about straight to one end of the screen, which is the opposite of what a screener is for.
This one is not a policy, it is a test. scripts/crypto-screen-check.js runs
on every deploy against a synthetic snapshot with deliberate holes in it, and it carries a selftest
that runs the same assertions against a zero-filling implementation to prove they fail. A check
that cannot fail is decoration.
“IV is in the 8th percentile” is not a fact until you know of what, and since when. The eighth percentile of two years and the eighth percentile of thirty-six years are different statements, and a number without its denominator lets you confuse them.
So every percentile ships beside n, the first date in the series, and the window it was measured over. Check it yourself:
That sample is a real response from
2026-09-21 and it will not match yours: n grows by one every session and both
percentiles move with it. Run the command. On a page about not showing you stale numbers as if
they were live ones, a frozen example has to say so.
Below eight samples a percentile is not published at all. The field comes back
null and the analyst says why rather than ranking three observations and calling it a
reading. A thin sample does not make the number uncertain, it makes it meaningless, and rounding
meaninglessness to a plausible-looking integer is how a data product starts lying politely.
When a collector stops publishing, the honest answer is that we are broken. So the
endpoint returns 503 with reason: "collector_quiet" and a Retry-After
header. It does not return 200 with an empty book.
The comment on that line in our own source says it better than marketing copy would: a 200 with zeroes would render a dead publisher as a calm book. A flat market and a dead feed look identical in a zeroed payload, and the difference is the entire trade.
Ask for today's gamma profile before the session's first pass and you get 200 with
banked: false and a sentence explaining that the key is scoped to one Eastern trading
day, so this is empty before the first pass rather than a claim that gamma was flat.
A 404 there would say your request was wrong. An empty array would say the session was
featureless. Both are claims we have no basis for. The same applies to a ticker with no logged
session history: logged: false, plus a note saying that is a statement about our
record, not about the ticker.
Dealer gamma describes the size of moves and the location of levels. It does not predict direction. That is not a disclaimer at the bottom of a webpage where an agent will never read it, it is a field in the response:
The history endpoints carry not_a_forecast for the same
reason: counts and medians over sessions we logged describe what happened, and an agent that drops
the note turns a description into a prediction. Putting it in the payload means the warning
survives the trip into someone else's model.
Freshness is a number, not an adjective. Every documented endpoint answers with the same three fields, so you can branch on the age rather than trust the word “live” in our marketing:
The third field is the one that matters, and almost nobody ships it. A timestamp with no provenance reads as a measurement whether it is one or not, so ours says which:
measured — the data carries this stamp. When the world was observed.
The dealer map computing its ladder, the CFTC printing its weekly report, the volatility
record closing a session.retrieved — we fetched it live for this request. When we got it,
not when it was produced. Honest for a passthrough, and materially weaker. We say so rather
than letting you assume the stronger one.unknown — we could not source a real timestamp, so as_of is
null. Never the request clock.That last rule is the whole discipline, and we failed it until 2026-09-21. Four endpoints were
stamping the moment the JSON was assembled and presenting it as the age of the data. Three of them
answered an upstream failure with { updated: now, events: [] } — a fetch
error rendered as “checked just now, nothing scheduled”. A fourth put
updated: now on CFTC positioning, which is published weekly and can honestly be
six days old. That is a zero-fill wearing a clock, and it is precisely what this page argues we do
not do. It is fixed, and there is now a deploy gate that fails the build if the shape comes back.
An upstream failure now nulls the collection rather than emptying it, so
events: null and events: [] can never be confused — one means the
fetch broke, the other would mean the week is genuinely clear.
Because it is checkable, and because most of the industry fails it. Zero-filling is not usually malice, it is a schema that had no way to say “unknown” and a pipeline that needed a number. The result reaches you as a measurement, and there is nothing in the payload to tell you it was never measured.
We would rather hand you a null, a 503 and a sentence about why than a clean-looking
row you cannot trust. It makes our responses slightly harder to consume and considerably harder to
be wrong with. If you find a place where we break one of these rules,
that is a bug and we want to hear about it — tell us
and we will fix it or document why it is not what it looks like.
Related: the error contract · live status · the MCP & API · how the map is built
Market data on this page is delayed and provided for general information only. It is not financial advice or a recommendation to trade. VIX/VXN/RVX are ~15-minute delayed (CBOE); index values use E-mini futures. Options trading involves significant risk of loss. © 2026 NoVo Options Trading LLC, d.b.a. NoVo Crypto Trading.