On a Tuesday morning in February, a research pipeline returned a nine-dimension analysis of a cryptocurrency protocol. It had sections for technical architecture, token economics, market positioning, ecosystem role, regulatory exposure, team and governance, risk matrix, narrative positioning, and supply-chain transmission. Every section was populated. Every section was labeled N/A.
The input to that pipeline was zero bytes.
Not corrupted bytes. Not a truncated payload. Not a paywalled page that returned a 402. Zero. The scraper had failed upstream, the parser had emitted an empty array, the classifier had tagged the domain as "unclassified," and the analysis stage had โ silently, without an exception, without a warning line in the log โ proceeded to generate a complete, formatted, professionally structured document describing nothing. The report was 1,847 words long. It contained no false statement about any project, because it contained no statement about any project. It was, in the most literal sense, unfalsifiable.
That is the failure mode worth dissecting. Not the scraper. The gate that was not there.

Crypto research has industrialized. In 2020 I audited Curve Finance v2 by hand โ forty hours of reading the stableswap invariant logic against the whitepaper, tracing fee distribution across three edge cases where rounding errors leaked value into arbitrage. In 2026 that same work is performed by a chain of six or seven models, each consuming the previous one's output, each validated by a schema, and almost none of them validated by a human until the final artifact lands in a distribution channel with a buy-or-avoid tag attached.
The architecture is standard. Acquisition: scrapers, RPC calls, subgraph queries, paid data vendors. Parsing and entity extraction. Classification and routing โ is this DeFi, is this NFT, is this a Layer 2, is this out of scope. Dimensional analysis. Synthesis and publication.
Each boundary between layers is a contract. And like every contract in software, it carries an implicit assumption about what the counterparty will deliver. Acquisition assumes the network is up. The parser assumes the payload is HTML and not a Cloudflare interstitial. The classifier assumes it received text. The analysis stage assumes it received information.
The analysis stage's assumption is the one that fails silently, because it is the only assumption that can be satisfied by nothing.
A parser handed a 403 will usually throw โ the selector returns null, the .text call on a missing element raises. A classifier handed an empty string will usually return "unclassified," which is a legitimate answer to a legitimate question. But an analysis stage handed an empty list of information points has nothing to raise on. It iterates over zero items. Zero iterations is not an error in any language I know. It is the correct result of the correct operation on the correct input.
And then, because the prompt template demands nine sections, it produces nine sections.
Consider the actual validation table in that report. Eleven rows. Title: missing. Source: missing. Domain tag: missing, and inconsistent with the pipeline's configured scope. Core thesis: three fields, all blank. Information points: empty array. The table was honest. It marked each row with a cross and the string "not provided."
Two paragraphs later, the same document stated that the analysis could not be executed. And then it executed it anyway, reserving nine numbered sections for the dimensions it could not analyze, filling each with N/A, and closing with a comprehensive assessment whose core judgment was, verbatim, "N/A โ this input contains no analyzable content."
Read that again. The pipeline correctly identified that it had no input, correctly concluded that analysis was impossible, and then produced a formatted analysis of the impossibility. It was self-aware and it was still wrong, because self-awareness was rendered as a section header rather than raised as an exception.

This is a type error expressed as a formatting decision. In Python, the distinction is Optional[List[InfoPoint]] versus List[InfoPoint]. If the field is typed optional, then None and [] are both legal, and every downstream consumer must handle both. If the schema itself encodes the invariant len(info_points) >= 1, then the empty case cannot be represented, and the pipeline fails at the boundary instead of at the point of publication.
Most production pipelines choose the optional type, because the optional type never fails, and a pipeline that never fails looks like a pipeline that works.
In JavaScript the trap is sharper. The idiomatic guard if (!infoPoints) throw new Error(...) does nothing, because [] is truthy. The empty array sails through the check that was written specifically to stop it. The correct form is if (infoPoints.length === 0) throw, and the difference between the two is the difference between a gate and the appearance of a gate โ which is the entire subject of this piece.
The same shape recurs in on-chain data. Query a subgraph for the deposit events of a pool whose contract has not yet been deployed and you do not receive an error. You receive []. An empty result set is a valid answer to a valid GraphQL query. The indexer is working. The schema resolved. The transport succeeded. Everything is fine except that the pool does not exist, and nothing in the response distinguishes "this pool has zero deposits" from "this pool has never existed."
The minimal fix is three lines: