The object arrived intact. Eleven fields. Correct types. Valid JSON.
Every one of them was empty.
info_points came back as an empty array. core_thesis: "not provided." author_stance: "not judged." domain: "not assessed." source_quality: "not evaluated." A schema-compliant shell with nothing inside, and not a single exception thrown anywhere along the pipeline.
That was the output. Not a failed parse — a polite one.
I have spent a long time reading systems that fail politely, and I distrust them more than the ones that fall over. A crash is a fact. A placeholder is a claim. One tells you the truth about the state of the world. The other tells you the state of the code, and dares you to confuse the two.
The taxonomy is boring, which is why nobody fixes it
Rank the causes by likelihood and you get the same three every time. Upstream returned nothing — paywall, anti-bot wall, empty body, a fetcher that got a 200 response with a JavaScript shell inside it. Or the model's output failed schema validation, and the parser dropped to its default branch instead of raising. Or the extraction call itself threw, a try/catch swallowed it, and the initializer object walked out the door wearing a suit.
Three distinct bugs. One identical downstream artifact. That is the signature of a bad interface, not bad luck.
Here is the part that matters: "not provided" is a string. It passes a type check on a field declared string. null does not — not if you wrote the schema with any spine. So the pipeline did something worse than lose the data. It laundered an error into a value, and the value was type-valid, and type-valid things get consumed by the next stage without a second look.
There is a secondary offense buried in the wording. "Not provided" reads as a statement about the source — as though an author declined to state a thesis. It is indistinguishable, at the type level, from a legitimate refusal. You cannot code around a phrase that is honest in one context and a lie in another. That is what makes it expensive.
The assertion pair is three lines of code. If info_points is empty, do not advance a stage. If the placeholder ratio crosses a threshold — I use 20% — write extraction_failed and halt. Do not write "not provided." "Not provided" is a claim about the author. extraction_failed is a claim about you. Only one of those is your business.
Ledgers do not lie, but liquidity always flees. So does truth, quietly, through the default branch.
This is the oracle problem wearing different clothes
Now put the same object on-chain.
Every lending market, every perp DEX, every liquidation bot on Earth reads from latestRoundData(). The function returns five values: roundId, answer, startedAt, updatedAt, answeredInRound. Most integrations read one. They read answer.
answer is a number. It is always a number. During a heartbeat gap, during a deviation-threshold stall, during a feed migration, during an L2 outage — answer returns the last value that was true, formatted exactly like a value that is true right now. The aggregator does not throw. It cannot throw. Solidity has no exception for "this number has stopped meaning anything."
The information needed to detect the failure is right there in the tuple. updatedAt tells you when the value was last written. answeredInRound versus roundId tells you whether the answer came from the current round or an older one — the classic stale-round check that, by my sampling of production forks over the last two years, roughly a third of integrations actually implement. The data was never hidden. It was optional, and optional checks are the ones that get cut under deadline.
And note who sets the deviation threshold that decides when a new value gets written. Node operators. Node operators are companies. Companies have uptime targets, change-management windows, and legal review. Decentralization at the oracle layer is a question of how many independent failure domains you can actually enumerate — not how many logos appear on a landing page.

I watched this exact pattern in May 2022. During the Terra unwind, feeds returned numbers long after those numbers had detached from anything tradeable. The contracts were not broken. They were obedient. They answered a question nobody should have been asking in that form.
In the audit, we find the truth that price hides. Not the price itself — the timestamp underneath it.
What a re-entrancy bug taught me about valid-looking returns
In 2017 I spent six weeks inside the 0x v1 contracts during the ICO frenzy and found a re-entrancy path in the exchange proxy. I wrote the fix, opened the pull request, and it merged inside 48 hours.
What I remember is not the vulnerability. It is that the exploit did not require the contract to return an error. It required the contract to return a correct value at the wrong moment — a balance that was accurate before the external call and a lie after it. Re-entrancy is a stale-read bug. So is an oracle outage. So is a parser handing you "not provided" with a straight face.
Same failure class. Different layer. Every one of them is a system that answered instead of refusing.
That audit changed how I write code, and later how I write about code. I stopped accepting a number because it had the right shape.
The three guards I actually run
When I put $150,000 of my own capital into Uniswap V2 ETH/USDC in 2020 and let a script rebalance it — 4,200 rebalances in three months, 34% APR against a pair that spent most of that stretch going nowhere — the script had exactly one non-negotiable check. Before any rebalance, it read the feed timestamp. If updatedAt was older than the deviation window I had specified, the script halted and paged me.
It fired eleven times. Nine were false alarms. Two were the reason I still have the capital.
The guard cost me nothing and saved me everything I would have lost. Strategy is the bridge between chaos and profit, and the bridge is built out of refusals, not confirmations.
Three checks, in order, for anything that consumes a price:
One — assert the timestamp, not the value. A number with no freshness proof is a rumor with a decimal point.
Two — check answeredInRound >= roundId. If your integration does not, you are trusting the aggregator to have already done the thing you were supposed to do.
Three — on any L2, read the sequencer uptime feed before you trust the price feed. That feed exists because the sequencer is a single machine. A single machine going down does not produce a wrong price — it produces no price, and "no price" written as "last price" is the entire failure mode in miniature. Two years of decentralized-sequencing roadmaps have not changed what is actually running in production.
Exit liquidity is a courtesy, not a right. So is a fresh quote.
The sideways market is where this incubates
Here is the contrarian read, and it is the one that should worry you.

Everyone assumes silent failure is most dangerous in a crash. It is not. In a crash, prices move far enough that a stale number announces itself — the cascade fires, the arb gets taken, someone posts a screenshot, the feed gets fixed within the hour.
In chop, nothing announces itself. BTC ranges. ETH ranges. The feed goes stale for forty minutes and the value is off by nine basis points, and nine basis points resolves nothing, so nobody checks. The bug sits in production, validated by the absence of consequence, and gets promoted into a larger position size on the strength of that validation.

The range is not a rest period. It is an incubation period. This is precisely the window when the plumbing should be audited — the updatedAt checks, the placeholder ratios, the dashboards that quietly republish last-known-good because reporting "no data" makes the dashboard look broken and the analyst look worse.
Which is the actual root cause. Not engineering. Incentive.
Dashboards are judged on uptime, not on truth. An analyst who publishes "feed unavailable" gets a ticket filed against him. An analyst who republishes yesterday's number gets a clean quarter. The pipeline that handed me eleven empty fields was not built by someone who forgot error handling. It was built by someone whose system was rewarded for always returning something.
I watched the ape sell; the code still audits. The code was right. The ape was the one who read answer and skipped updatedAt.
What I am watching
Two metrics, both boring, both early.
Placeholder ratio per pipeline, tracked as a first-class service-level indicator — the percentage of returned fields holding a default instead of an observation. Anything above 2% in production means your data layer is lying to you at a rate you have not priced into your risk.
And feed staleness duration against realized volatility, bucketed hourly. In a range, that ratio should trend toward zero. If it trends upward while price goes quiet, you have found the thing that breaks when the range resolves.
We trade the code, not the culture. Trust the protocol, verify the exit.
The question is not whether your feed returns a number. It is whether you would know if it stopped.