I spent the last several months building out a financial data API
(XFINLAB) that pulls from about 20 different government/exchange sources
— SEC EDGAR, FINRA, CFTC, FDIC, USDA, CBOE, EIA, openFDA, CPSC, a couple
of crypto exchanges. Along the way I made one architectural rule that
ended up shaping almost every collector module: a field is either a
real value with a source, or it's null with an explicit reason. Never
an estimate presented as if it were real.
This sounds obvious written down. In practice it's a real design tax.
The tempting shortcut
Say you're building a collector for USDA agricultural commodity prices.
The USDA Quick Stats API doesn't always have this year's data yet for a
given commodity — sometimes the most recent real observation is from 8
months ago. The tempting move is to interpolate: draw a line between the
last two known points, guess where "now" would sit on it, ship a number.
It looks like better data. It demos better. And it's fabricated.
What we did instead
Every collector in this codebase follows the same shape:
def get_context_for_ticker(ticker: str) -> Optional[dict]:
if ticker not in _TICKER_TO_SOURCE_MAPPING:
return None # no linkage, not a guess
...
if fetch_failed:
return _load_persisted_fallback(ticker) # last REAL value we saw, honestly dated
return real_fresh_value
No collector has a code path that produces a plausible-looking number
that isn't traceable to an actual source and an actual fetch timestamp.
When a value truly isn't available, the API returns an explicit
"unavailable" with a reason (not configured / rate-limited / genuinely
no match for this ticker) — not a zero that looks like a real zero, and
not last year's number silently relabeled as current.
Making it checkable, not just claimed
Every collector self-registers into a small SQLite-backed registry at
import time:
register_source("usda_agriculture", "USDA Agricultural Commodity Prices", "agriculture")
with record_run_start / record_run_success / record_run_error
wrapping the actual fetch calls. That registry backs a public,
unauthenticated status page — https://www.xfinlab.com/trust.html — so
"this data source is honestly reporting, not guessing" isn't just a
claim in the docs, it's something anyone can go check live, updated in
real time, including which sources are currently down.
Why this matters more in 2026 than it used to
Q1 2026 reportedly saw $2.3B in trading losses tied to AI-generated
misstated earnings figures, and FINRA's 2026 Annual Oversight Report
devoted its first-ever dedicated section to AI/hallucination risk for
broker-dealers. 62% of enterprise AI users now cite hallucinations as
their top deployment barrier. None of that is about financial data APIs
specifically, but it's the same underlying failure mode: a plausible
number that isn't real, presented with no way to tell the difference.
The fix isn't really an AI problem to solve with a better model — it's
an interface design problem. Make the "I don't know" path a first-class,
explicit response, not a bug to route around.
If you want to see it live
The API's free tier is instant, no card — 20+ endpoints, Python/JS SDKs,
and an MCP server if you're wiring this into an agent:
https://www.xfinlab.com/intelligence-api.html
Curious how other API builders here handle this same tradeoff —
especially anyone working with genuinely gappy upstream sources. Do you
interpolate, forward-fill, or null-and-explain?
Top comments (5)
This is exactly the design principle we ended up converging on too, from a different direction — a "not found" from a malware-hash lookup or a URL-reputation check explicitly says not found ≠ safe rather than implying a clean bill of health, and a best-effort DKIM check explicitly states that a non-match doesn't prove DKIM is unconfigured (we only test common selectors, not all of them). Same underlying rule as yours: the absence of a positive finding is not itself a finding, and collapsing that distinction is how "no result" quietly becomes "verified safe" in someone's head six months later. The trust-status-page idea is smart too — makes the honesty checkable instead of just claimed, which is a good answer to "why should I believe your nulls are real nulls."
Really like the DKIM example — "we only test common selectors" is exactly the kind of scoping honesty I mean. A non-match that silently reads as "not configured" when it's actually just "we didn't check the selector they're using" is the same failure mode wearing a different hat.
The absence-of-finding-isn't-a-finding framing is a better way to put it than how I had it in my head, honestly. Going to steal that phrasing.
Curious — did you build the trust-status-style page for your own checks too, or is it more of an internal thing right now? Making it externally checkable was the part that took the most convincing internally for us (easier to just... not expose the failure modes), but it's been the right call.
Honestly, more internal right now — no dedicated public status page yet. What we do have is adjacent though: the checks run as a scheduled GitHub Action against the live API (not mocked), and since the repo's public, anyone can see the pass/fail history directly. A failure also auto-opens a public GitHub issue rather than just paging us quietly — which we actually only discovered works by deliberately breaking it once to test the alerting itself, not by trusting it worked. So there's real transparency in the mechanism, just not packaged as a dedicated page someone would think to check on its own.
Your "easier to just not expose the failure modes" instinct is probably the same resistance we'd hit if we built a proper one — right now our third-party dependencies (a phishing-DB lookup, a malware-hash DB, a couple of others) genuinely do go down sometimes, and a dedicated status page would make that a lot more visible than it currently is, buried in Action run history. Would be curious what actually moved the needle internally for you — was it a specific incident, or just deciding the credibility upside outweighed the discomfort?
Honestly, no single incident — it was baked in from day one because of the domain. In finance, a wrong guess isn't just annoying like a bad autocomplete, it's the kind of thing that costs someone real money if they act on it. So the calculus was pretty simple early on: a field that says "I don't know" is worth more than one that's right 90% of the time and silently wrong the other 10%, because you can't tell which bucket you're in until it's too late.
The harder part wasn't deciding to do it, it was resisting the pressure to backfill nulls with "reasonable" defaults once real users started asking for cleaner-looking responses. That's usually where teams cave — the null is honest but ugly, and ugly-but-honest loses to some product manager who just wants the demo to look complete. We just kept saying no to that, which in hindsight was the actual discipline, not the initial decision.
The "resisting the backfill pressure" point is the part I'd underline — deciding to be honest on day one costs nothing, because there's no demo yet to make look worse. The real test comes later, when a real stakeholder is staring at an ugly null in a real product and asking why it can't just be a zero. That's a different kind of discipline than the initial design choice, and it's the one that actually erodes over time if nobody's watching for it.
We haven't been tested on that axis yet, mostly because we're pre-traction enough that nobody's pushed back on an ugly response for looking incomplete. Which is a real difference worth naming honestly — you're describing discipline under real pressure, we're describing a principle that hasn't been pressure-tested. I'd rather say that plainly than imply we've held a line that hasn't actually been pulled on yet.