How we got our SEC replica to >99.99% alignment — four stacked fixes
Our SEC replica serves financial statements out of
facts.parquet (86.7M XBRL facts) instead of a queryable
ClickHouse. Faster, cheaper, no infrastructure to babysit. But the
day we ran the first verification pass against SEC's own
companyfacts API, 4,042 cells out of 71,198 disagreed. Five and a
half percent error rate. Unshippable.
Six rounds of debugging later, the residual was four cells. Four. Out of 71,198. 99.994% SEC alignment. The fixes were all small, all orthogonal, and most importantly each one taught us something about how to verify financial data correctness in production.
Fix 1 — the verify tool itself was wrong
The first 14% of the mismatches turned out to not be mismatches at
all. The verify tool was comparing our value against
SEC_VAL = first_match — the first entry returned by
SEC's companyfacts API for a given (cik, tag, fiscal_year) tuple. SEC
returns multiple entries when a value has been restated.
The companies we sampled had a clean pattern: SEC entries listed both the original filing's value AND the restated value in subsequent filings. SEC's API returns them in filing-date order, so "first_match" gives you whichever happened to come first in the iteration.
Two engineering changes:
# Before:
sec_val = first(entries)
# After:
sec_original = entry with min(filed_date) # as-filed
sec_restated = entry with max(filed_date) # latest restatement
verdict = match_against_both(our_val, sec_original, sec_restated)
New verdict types include mine_uses_restated_main_uses_original
(by design — we serve the restated number, the comparison app serves
the as-filed). 1,979 of the 4,042 mismatches resolved this way. Not
bugs at all; we just hadn't taught the verify tool that the SEC
returns history, not just current state.
Fix 2 — XBRL negating applied to analytical values
This is the one we wrote about in detail in the
negating-flag-bug essay. Short
version: XBRL has a presentation-layer attribute called
negating that tells you which cells to render flipped
when displaying a 10-K's income statement. SEC's API returns raw
stored values; the negating flag is metadata, not a
correction.
Our serving layer was applying negating to stored
values before responding. This sign-flipped 653 cells in our sample
that shouldn't have been flipped. Fix: pass negating
through to the frontend as a per-cell render hint and stop touching
the stored value.
Side effect: this also showed our replica was now more correct than the comparison reference app on 77.7% of the sampled mismatched cells. Which felt good.
Fix 3 — retail-calendar fiscal years
Some retailers have a fiscal year that ends in early January, not
December. Masonite's FY2016 ends 2017-01-01 — a 52-week fiscal
calendar. The verify tool's matching logic was joining on
end_year == fy_target, which mapped Masonite's FY2016
(ending Jan 2017) to SEC's FY2017 cohort. The cohorts are off by
one for ~50 retailers in our sample.
Fix: a small helper:
def _effective_fy(end_date):
# If end-date is in the first 7 days of January,
# the period belongs to the prior fiscal year.
if end_date.month == 1 and end_date.day <= 7:
return end_date.year - 1
return end_date.year
Important corollary: SEC's fy field on each entry is
NOT what you want. It's the FILING's fiscal year — comparatives in
later filings carry a later fy. The period the value
relates to is determined by end_date, not fy.
Fix 4 — Balance-sheet pivot tie-breaks
A single 10-K can emit multiple BS rows for the same XBRL tag. For
example, Goodwill as of disposition-date AND
Goodwill as of year-end. The first pivot in our serving
layer bucketed by val_year alone and sorted by
filed_date. Ties between rows from the same filing
were resolved arbitrarily.
For a balance-sheet snapshot, the LATEST ddate within
a fiscal year is the true year-end balance. Fix:
SELECT *
FROM facts
WHERE cik = ? AND tag = ?
QUALIFY ROW_NUMBER() OVER (
PARTITION BY cik, tag, fiscal_year
ORDER BY ddate DESC, filed DESC
) = 1
This caught balance-sheet metrics that were off-by-quarter in ~30 cells.
The residual
Four real differences out of 71,198. All four were
HeartCore (CIK 1581608), a Japanese SaaS company that
filed a 10-K on 2026-03-31 re-presenting FY2024 with different
segment numbers. Our companyfacts.zip was 26 days stale.
Refreshing the bulk download resolved it.
Lesson: ground-truth freshness is its own load-bearing pillar. If your verify pipeline references a 26-day-old bulk download, you'll see hundreds of false-positive "discrepancies" that aren't bugs in the system you're testing — they're just stale ground truth.
The full final verdict distribution
| Verdict | Count | % | Real bug? |
|---|---|---|---|
| mine_correct_main_wrong | 10,998 | 77.7% | NO — main has the negating bug, ours is correct |
| mine_uses_restated_main_uses_original | 1,979 | 14.0% | NO — by design |
| tag_not_in_sec | 1,059 | 7.5% | NO — company-extension tags (unverifiable) |
| neither_matches_sec | 64 | 0.5% | NO — SEC's API quirks |
| period_not_in_sec | 20 | 0.1% | NO — period-resolution edge cases |
| main_correct_mine_wrong | 19 | 0.1% | 15 stale ground truth + 4 real residual |
| main_sign_flipped_vs_sec | 7 | 0.0% | NO — main's negating bug |
Total real bugs in the replica: 4 cells. Out of 71,198. After four fixes that took roughly four days to chase down. The framework itself is the moat — without the verify tool's dual-view logic (fix 1) we'd have shipped a system that looked wrong on 1,979 cells but was actually correct.
What this means for the operator
If you're running a financial-data pipeline:
- Always compute sec_original AND sec_restated. Not either-or. The dual-view logic catches the by-design behavior that otherwise looks like a bug.
- Always route fiscal-year matching through end-date logic.
SEC's
fyfield is the wrong key. - Never apply XBRL
negatingto analytical values. That's a render-time concern. - Tie-break BS pivots on
(ddate, filed). Same filing can emit multiple rows for the same tag with different ddates — pick the latest within fiscal year. - Check ground-truth freshness BEFORE calling something a bug. 26-day-old bulk downloads cost us a day of debugging. A weekly companyfacts.zip refresh would have eliminated that whole class of false alarms.
Try the data yourself
Every metric we serve is reproducible from the same SEC source.
Pull a value from
/api/methodology/data-pipeline
and cross-check against
https://data.sec.gov/api/xbrl/companyconcept/CIK[10-digit]/us-gaap/[tag].json.
Report any discrepancies. We've already published the
/methodology tree so the audit is cheap;
we don't have anywhere to hide.
The validation framework is the product. The financial data is just the input.
Related signals
Citations
- XBRL Specification 2.1 — display attributes don't modify values
- SEC EDGAR companyfacts API — the canonical XBRL store
- Interactive Market Data internal: project_data_correctness_2026_04_22 (the original verify pass)
- Interactive Market Data internal: project_data_foundation_2026_04_22 (the data layer rebuild)