A Quiet Failure Looks Exactly Like a Clean Result
Ask any data-broker lookup whether a person appears on a given site and you get back found or not found. Two states. It is the obvious shape, it matches the question, and for most purposes it is fine.
It is not fine here, and the reason is what people do with a negative.
A false negative from a search engine costs you a scroll. A false negative from an exposure check costs you the deletion request you never filed, on a listing that stays up for another year. In this product the negative is the actionable answer — it is the one that ends the work. So it has to be worth something. And a negative meaning we could not tell is worth nothing while looking exactly like a negative meaning we looked and it is not there.
Three states
Every successful call returns one of exactly three:
indexed— a listing matching the identity was found on the domain, andevidencecarries the URL, title, snippet and retrieval timestamp that justified it.not_indexed— the name shape, the broadest query the endpoint issues, ran against the domain and matched nothing.indeterminate— we looked and cannot call it.
The narrowness of not_indexed is the entire point. It is the only state that reports an observed absence, and it can only be returned when the broadest available query actually ran and came back empty.
There are three ways to reach indeterminate: retrieval failed; the evidence was too weak in either direction; or the query we issued was scoped to a single identifier the caller declared and the name shape never ran — so a miss says only that the domain's index does not answer to that identifier.
indeterminate returns 200. It is an answer, not an error. That trips people up, and it is deliberate: an error is something that went wrong on the way to an answer. This is the answer.
The obvious objection
"
indeterminateis a cop-out. You have invented a state that lets you never be wrong."
It is the opposite, and the direction is what gives it away.
In a two-state world we can return not found any time we do not have a hit. That is free. In a three-state world not_indexed has to be earned: no name shape, no absence claim. We gave up the cheap negative and kept the expensive one.
The test of whether a taxonomy is honest or evasive is which side the extra category makes life harder for.
The same bug, one level up
A verification asks about one domain. A scan sweeps an identity across many and returns one card per distinct exposure.
Now run the same sweep twice. The first reaches every domain it planned to. The second loses three — a provider hiccup, a rate limit, a timeout. Nine domains held a record either way.
Both runs return nine cards. The card lists are identical. Nothing in a list of things that were found encodes anything about the things that were never looked for.
So every scan returns a coverage block alongside the cards: how many domains were expected, how many were queried and hit, how many were queried and came back empty, how many failed, how many were never reached — and it names the ones it never got to.
"coverage": {
"expected": 548,
"queried_hit": 9,
"queried_empty": 3,
"queried_failed": 0,
"never_queried": 536,
"never_queried_domains": ["advancedbackgroundchecks.com", "anywho.com", "..."],
"never_queried_truncated": true
}
queried_empty and never_queried are opposite in meaning
queried_empty means we asked and the domain holds nothing on this identity. The scan is correct to show nothing there, and you may act on the absence.
never_queried means we did not ask. The silence is ours and says nothing about the domain.
One is a fact about the domain. The other is a fact about us. Before this block existed the two were indistinguishable in the response, which quietly made the first one unusable — if you cannot tell an observed absence from an unobserved one, you have to treat every absence as unobserved, and nothing there collapses into nothing you can act on.
Two details that fell out of it
The block is null, not zeroed, on a run old enough to predate coverage recording. expected: 0, never_queried: 0 reads as no gaps. It means not known. That is precisely the bug we had just finished fixing, one layer up, and it would have shipped if we had let the struct default.
The block carries counts and domain names only. A site: target identifies who we asked, never who we asked about, so no part of the identity the caller sent can reach it. Coverage is publishable exactly because it describes our reach rather than their subject.
What this costs us
Publishing coverage means publishing the number twelve next to the number 548. A base sweep queries twelve pinned people-search domains; coverage is reported over a 548-domain registry derived from the California CPPA public data-broker register plus a curated priority set.
That is a bad slide. Anyone claiming hundreds of sites wins that slide every time.
We took the trade, because the number is not the claim — the accounting is. A sweep across hundreds of domains that returns a card list you cannot distinguish from a partial run has told you less than a sweep across twelve that shows its work.
The gap we have not closed
We report index presence. We do not fetch the broker page. So we cannot distinguish the listing is live right now from the search index holds a stale entry for a page that has since been pulled.
A mature version of this wants two further states — indexed_not_live and not_indexed_live — and they do not exist, because returning them would mean asserting a fetch we never performed. That sentence ships inside every response as contract.limitation, so a downstream consumer cannot miss it even if a marketing page of ours is sloppier than the API is.
That is not modesty. It is the same rule as everything above: do not return a state you did not earn.
None of this is really about data brokers
Any system that answers from sources it does not control has this failure mode, and it has the same shape every time. Partial retrieval produces a confident, well-formed, complete-looking response.
A RAG pipeline that got three of ten chunks returns an answer indistinguishable from one that got all ten. An agent whose tool call timed out reports what it found. A monitoring sweep that could not reach two hosts shows two fewer alerts, which looks like good news.
In each case the wrong answer is loud and the quiet one is dangerous, because a quiet failure does not visibly degrade the output — it degrades it into something that looks better. Fewer alerts. Fewer cards. Nothing found.
The fix is not better retrieval. Retrieval will be partial sometimes no matter what you do. The fix is structural: make it impossible for a partial run to be mistaken for a complete one, in the response body itself, without the caller having to take your word for it. Return what you reached, what you did not, and which absences you actually observed.
Then the caller can decide what a silence is worth, instead of guessing.
The Sirveil Exposure API is documented at ai.sirveil.ai/docs/api, with an OpenAPI 3.1 spec and a Postman collection. It is metered through AWS Marketplace. United States coverage only.