The data model
Dimensions, provenance, honest absence, coverage, and the change feed's guarantees.
Bixel's spine is simple: a Company has Facts, facts point at Captures, and confirmed transitions between facts become ChangeEvents. Categories group companies for comparison.
Dimensions
Every fact lives in one of ten dimensions, always in this order:
pricing · features · security · positioning · stack · hiring · reliability · funding · location · legal
Fact keys are dot-paths inside a dimension (pricing.tier.starter.monthly_usd, features.integration.langchain, security.cert.soc2-type-ii). Keys are stable and machine-checkable; the key registry is enforced at write time, so a key you see once will mean the same thing next month.
Fetching one dimension at a time (?dimension=pricing on the facts endpoint, or the dimension argument on MCP tools) is the cheapest way to work: full company records are large, and most questions only need one dimension.
Provenance
Every fact carries provenance, one of exactly two values:
observed— Bixel measured it independently: response headers, DNS, TLS, detected technologies, ATS job boards.company_stated— the company published it and Bixel extracted it from their page: pricing tables, feature claims, security pages.
The two are deliberately kept apart, and the /claims endpoint (check_company_claims over MCP) cross-reads them: what a company states versus what is observable. Where both provenances carry the same key, that is dual measurement, not a contradiction.
Every fact also carries:
as_of— the capture date the fact was last supported by. Facts are only as current as their last capture;as_ofsays exactly how current that is.source_url— the page that actually stated it.confidence— extraction confidence in [0, 1].
Honest absence
A missing fact means "not captured", never "false". Bixel does not infer values it has not seen, and endpoints answer gaps explicitly: a fact with no stored capture behind it answers proof_not_available rather than pretending, and companies whose sites block crawling carry visibly thinner records rather than padded ones.
Coverage
Coverage varies by company, structurally: category-by-category growth always trails thin coverage behind a dense core. So depth is contract, not inference: every company payload carries a coverage block —
"coverage": {
"dimensions_captured": 7,
"dimensions_total": 7,
"per_dimension": { "pricing": 11, "features": 42, "security": 17, "positioning": 7, "stack": 35, "hiring": 4, "reliability": 3 },
"thin": false
}per_dimension is zero-filled — absence is explicit, never by omission. thin is the honest coarse bit: fewer than 3 dimensions captured (calibrated against the real distribution — capture-blocked members sit at 1–2, everything with a working crawl at 3+). Category payloads carry it too: a per-member { dimensions_captured, dimensions_total, thin } plus a category-level rollup { members, deep, thin }. Read depth from these blocks, never from raw fact counts.
Jobs: ATS-tracked vs page-stated
Hiring is measured two ways on purpose:
jobs.active_postings(and the/jobsendpoint) — live postings from the company's ATS board (Ashby, Greenhouse, Lever...). Only companies with a detected board have this; it is precise, dated, and includes compensation where stated.hiring.open_roles(a fact) — what the company's careers page states, extracted like any othercompany_statedfact.
A company can honestly show 0 tracked postings and a careers page claiming open roles: no board is detected, so the postings count is not tracked while the page statement stands. The payload says so itself — the jobs block is labeled:
"jobs": { "active_postings": 0, "ats_tracked": false, "note": "Job postings are not ATS-tracked for this company; hiring.open_roles reflects their careers page." }Read the pair as coverage information, not contradiction.
The change feed
/v1/changes (and per-company /history) is the dated event log. Every event is labeled with how it was derived, and the label is the feed's honesty mechanism:
derivation | Meaning |
|---|---|
confirmed | A live change confirmed by two independent captures. The pipeline holds a first observation as pending, invisible to every public surface, until a strictly newer capture shows the same state; a revert before confirmation discards it. Confirmed events cannot flap. |
historical | A diff between two web-archive generations, dated by the later generation. Real and dated, but archive sampling is sparse: treat dates as "by this date", not "on this date". |
baseline | First observation when a company entered tracking. Not a company change. Hidden by default. |
legacy | Events from earlier pipeline generations. Hidden by default. |
orphaned | The trailing part of an event chain the current record no longer corroborates (reprocessing artifacts). Hidden by default. |
The default feed serves confirmed + historical only. Everything else stays queryable via ?derivation= — the data is inspectable, it just never poses as live change.
detected_at is always the first-observation date (the capture day), not the date the pipeline wrote the row. Because confirmation waits for a second capture, an event can appear in the feed a few days after its detected_at.
Events carry significance (normal or high) and, authenticated, old_value / new_value / summary. The access ladder: keyless callers see that something changed, its key, and when — capped at 90 days / 50 events; a free key unmasks values within the same 90-day window (with working cursors); Pro unlocks the full archive. Depth is the paid axis, and it is never laddered above Pro.