A multi-stage analysis pipeline ran to completion last week. It produced nine analytical dimensions, forty-two tables, and zero facts.
Every field came back as a placeholder. No article title. No source. No core thesis. The most important field โ the list of extracted information points โ was an empty array. The pipeline did not crash. It did not throw. It did not page anyone. It emitted a clean, well-formed, schema-valid document that said N/A forty-two times and then declared itself finished.
That is the anomaly worth writing about. Not the missing data. The fact that a system built to detect risk could not detect that it had nothing to chew on.
I have spent twenty-five years reading code that fails badly. Most of it fails loudly. This failed politely. And polite failures are the expensive ones.
Where this architecture comes from
Two-stage pipelines are the default shape of crypto analytics in 2026. Stage one decomposes raw text into structured facts. Stage two reasons over those facts. The pattern powers trading signal feeds, protocol due-diligence bots, and โ increasingly โ the agent frameworks now executing on-chain transactions.
The design is seductive. Separation of concerns. Cheap models for extraction, expensive models for reasoning. Every team building an "AI analyst" ships the same skeleton.
It is also a fault-tolerance nightmare, because stage two almost never validates that stage one actually produced anything.
Funded teams are shipping this right now. A hundred-million-dollar raise buys you a beautiful orchestration graph and almost never buys you an input contract. I have reviewed agent frameworks where stage two was a nine-hundred-line prompt and stage one was a twelve-line function with no validation. The asymmetry is the tell.
This is not a new disease. It is the same class of bug that has bitten every ETL system since the 1990s.
The mechanics of the silent pass
Here is the shape of the defect, stripped down.
def load_stage_one():
raw = read_json("stage1_output.json")
return {
"title": raw.get("title", ""),
"source": raw.get("source", ""),
"info_points": raw.get("info_points", []),
"projects": raw.get("projects", []),
}
def run_stage_two(payload): analysis = {k: analyze(payload, k) for k in DIMENSIONS} return {"status": "complete", "analysis": analysis} ```
Read that again. raw.get("info_points", []) returns an empty list when the key is missing. It returns the same empty list when the key exists and holds an empty list. It cannot distinguish absent from empty from unparsed. The default value is a lie the parser tells itself.

Then run_stage_two iterates over nine dimensions and fills each one. Nothing in that loop checks whether payload["info_points"] has length greater than zero. Nothing asserts a minimum. The function has no precondition. It has a postcondition โ status: complete โ and that postcondition is satisfied by an empty input as easily as by a full one.
The gas isn't the fee. It's the friction of poor architecture.
The correct behavior for stage two was not to produce a report. It was to halt. Refuse the input. Emit Unprocessable and stop the line. Every dimension it filled with N/A was compute spent converting nothing into something that looked like something.
To its credit, the model did not hallucinate. It labeled every gap explicitly. That is the right instinct, and it is rarer than it should be. But the orchestrator around it should never have let stage two run at all.
Watch the same defect in an oracle. A price feed aggregates submissions by taking the median. Feed it an empty set and a naive implementation returns zero. Zero is a valid price. The protocol reads it, liquidates, and moves on. No exception fired. No alert. This is not hypothetical โ it is the shape of half the oracle incidents I have read post-mortems for. Code that doesn't validate cardinality before it reduces is code that will one day reduce nothing into a number.
Vulnerabilities aren't discovered. They're scheduled. This one was scheduled the moment someone wrote a null-tolerant parser and never wrote the gate.
Why the gate never gets written
Because gates cost throughput. A validation step that rejects empty input will, in testing and in production, reject input you wanted. Someone adds a retry. Someone loosens the check to if raw is not None. The gate erodes.
There is a second reason. Empty input looks like success to the metrics that matter. The job ran. Latency was normal. Cost was low. Error rate stayed at zero, because the pipeline defined "error" as an exception, and there was no exception.
And it survives testing, because tests ship fixtures. A fixture has content. The null path is covered by the type system, not by a test case, so it is covered by nothing. Every green check mark in the suite is evidence about the happy path and silence about the empty one.
You cannot dashboard your way to a failure that reports itself as healthy.
I ran into the sharper version of this in 2026, integrating an LLM agent framework with a zk-rollup. The oracle feed accepted agent-generated payloads. A prompt-injection attack let a malicious agent fold fabricated data into the feed's output โ not by breaking cryptography, but by exploiting the assumption that a signed, well-formed payload was a truthful one. Two million dollars in the simulation. The envelope was valid. The contents were not.
Same defect. Different layer.
The blind spot is downstream
Here is the part that will cost real money.
Everyone reviewing this incident will blame the model. They will file it as a prompt-engineering problem, add a system prompt that says "never produce a report from empty input," and ship.
Wrong layer.
The model behaved correctly. It received nothing and said so. The vulnerability lives one hop downstream, in the consumer โ the dashboard that renders N/A as a neutral gray cell, the allocation bot that reads status: complete and proceeds, the agent that takes a structured response and acts on it. Optimization isn't about making the model smarter. It's about respecting the user enough to fail in front of them.
If you can parse a response and cannot tell whether it contains signal, you do not have a parser. You have a coin flip with good formatting.
The distinction that matters is semantic, not syntactic. unknown and zero are different types. absent and empty are different types. status: complete and status: meaningful are different types. Collapse them โ which is what every get(key, default) does โ and you have built a system that cannot tell truth from silence.
Takeaway
The next wave of protocol exploits will not come from reentrancy. Those are patched, audited, and boring. They will come from type confusion between "I don't know" and "I know it's nothing," executed at machine speed by agents that never learned to ask.
Code that doesn't fail loudly is code that's not ready for mainnet reality. Audit your parsers. Then audit the thing that reads your parsers.