Oracle Health Explorer

Protocols rarely lose money because an oracle was attacked. They lose it because the consuming contract called latestRoundData(), took the answer, and used it — and the answer was old, or negative, or from a round that never finished. This page reads live Chainlink Data Feeds through a contract on Base Sepolia that performs those checks and names the result.

BaseBase Sepolia · testnet only

This demo is read-only. There is no wallet to connect and nothing to sign: every function on the contract is a view, so the whole page is built from eth_call. You will not be asked for a signature and you will not spend any gas. The page also takes no input at all — there is no field, no slider and no query parameter — so nothing you send can change what it reads.

Checker 0x401a…CdB0 · Deployed in block 44827955, tx 0xa306…8578 · Source on GitHub

Watch: 30 seconds

The six things that can be wrong

A price feed does not usually fail by disappearing. It fails by continuing to answer, politely, with something a consumer should not act on. The contract distinguishes six states, and the order matters: they are declared least to most severe, and when more than one is true at once a consumer is told the worst of them rather than whichever the code happened to check first.

StateConditionWhy it matters
HEALTHYcomplete, positive, inside this feed’s max ageSafe to price against — for this round, not for the next one.
STALEage > maxAgeThe commonest failure and the quietest. The feed still answers and the data is well-formed; nobody has updated it. Code that only checks for a revert sees nothing wrong.
NON_POSITIVE_ANSWERanswer <= 0answer is a signed integer, and a production feed has printed a negative one. Cast to an unsigned type without checking, a small negative price becomes a number near 2256.
FUTURE_TIMESTAMPupdatedAt > block.timestampComputing now - updatedAt reverts under checked arithmetic, or underflows to an enormous age without it. Unlike staleness, waiting does not fix it.
INCOMPLETE_ROUNDupdatedAt == 0The round was started and never answered, so there is no timestamp to measure an age from at all.
UNAVAILABLEthe feed reverted, has no code, or answered in the wrong shapeReported as a verdict rather than propagated as a revert — a contract whose job is reporting broken feeds must not break on one.

What the chain says right now

Every figure below is read from the deployed contract on each page load: the verdict, the round data it was derived from, and the threshold it was judged against. Nothing here is a number this page decided. The raw values are shown beside each verdict so you can redo the arithmetic instead of trusting it.

Read at block 45526629. Worst state across all feeds: Healthy.

BTC / USD Healthy

Aggregator 0x0fb9…4298 · the pair name above is read from the aggregator itself, not from a label on this page

VerdictHEALTHY
Answer62994.42868459 (8 decimals)
Answer, as stored6299442868459
Round18446744073709847636
Updated at1786820710 (unix seconds)
Age13 minutes 54 seconds (834 seconds)
Max age for this feed1 hour 15 minutes (4500 seconds, immutable)
Headroom1 hour 1 minute before this round goes stale

What this means for a consumer: The round is complete, the answer is positive, and it is inside this feed's configured max age. A protocol reading this feed right now would get a number it can act on. That is a statement about the round, not a guarantee about the next one — which is why the check belongs in the consuming transaction rather than in a dashboard.
Off-chain commentary, written by hand. It is not an on-chain value and nothing about it is consensus-verified — the verdict and the figures above are.

ETH / USD Healthy

Aggregator 0x4adc…7cb1 · the pair name above is read from the aggregator itself, not from a label on this page

VerdictHEALTHY
Answer1881.17743277 (8 decimals)
Answer, as stored188117743277
Round18446744073709825345
Updated at1786821096 (unix seconds)
Age7 minutes 28 seconds (448 seconds)
Max age for this feed1 hour 15 minutes (4500 seconds, immutable)
Headroom1 hour 7 minutes before this round goes stale

What this means for a consumer: The round is complete, the answer is positive, and it is inside this feed's configured max age. A protocol reading this feed right now would get a number it can act on. That is a statement about the round, not a guarantee about the next one — which is why the check belongs in the consuming transaction rather than in a dashboard.
Off-chain commentary, written by hand. It is not an on-chain value and nothing about it is consensus-verified — the verdict and the figures above are.

LINK / USD Healthy

Aggregator 0xb113…5a61 · the pair name above is read from the aggregator itself, not from a label on this page

VerdictHEALTHY
Answer9.57830000 (8 decimals)
Answer, as stored957830000
Round18446744073709820998
Updated at1786821326 (unix seconds)
Age3 minutes 38 seconds (218 seconds)
Max age for this feed1 hour 15 minutes (4500 seconds, immutable)
Headroom1 hour 11 minutes before this round goes stale

What this means for a consumer: The round is complete, the answer is positive, and it is inside this feed's configured max age. A protocol reading this feed right now would get a number it can act on. That is a statement about the round, not a guarantee about the next one — which is why the check belongs in the consuming transaction rather than in a dashboard.
Off-chain commentary, written by hand. It is not an on-chain value and nothing about it is consensus-verified — the verdict and the figures above are.

USDC / USD Healthy

Aggregator 0xd30e…5165 · the pair name above is read from the aggregator itself, not from a label on this page

VerdictHEALTHY
Answer0.99988034 (8 decimals)
Answer, as stored99988034
Round18446744073709552768
Updated at1786774342 (unix seconds)
Age13 hours 6 minutes (47202 seconds)
Max age for this feed1 day 1 hour (90000 seconds, immutable)
Headroom11 hours 53 minutes before this round goes stale

What this means for a consumer: The round is complete, the answer is positive, and it is inside this feed's configured max age. A protocol reading this feed right now would get a number it can act on. That is a statement about the round, not a guarantee about the next one — which is why the check belongs in the consuming transaction rather than in a dashboard.
Off-chain commentary, written by hand. It is not an on-chain value and nothing about it is consensus-verified — the verdict and the figures above are.


Why the thresholds are not the same number

Each feed is judged against its own max age, fixed at deployment. That is a measurement, not a preference.

The three crypto feeds above republish every few minutes, driven by price deviation. USDC / USD does not: it moves on a roughly 24-hour heartbeat, confirmed across two consecutive intervals before this contract was written. A single estate-wide threshold cannot describe both. Tight enough for BTC, and USDC reads stale for almost all of every day. Loose enough for USDC, and a BTC feed frozen for twenty hours reads healthy. Neither of those is a staleness check.

So the threshold lives with the feed, and it is immutable — there is no owner, no setter and no upgrade path, which is why the max age shown in each table above is read from the contract rather than written into this page.

The boundary is the bug

maxAge is inclusive. An age of exactly maxAge is healthy; stale begins one second later. The comparison is age > maxAge, never >=.

That single character is the whole bug class, and it is invisible to testing that samples the middle of a range — both wrong versions agree with the right one everywhere except on one value. So the property harness constructs both sides of the threshold on every run rather than waiting to stumble onto it, and a companion test demonstrates that an interior sample does not catch the substitution.

A check that is still copied, and cannot fire

Older Chainlink documentation suggested guarding staleness with answeredInRound < roundId, and that line is still copied into consumer code today. All four aggregators here still expose answeredInRound — and on every one of them it comes back equal to roundId, so the guard can never fire.

The contract documents that in a comment instead of implementing it. A check that always passes is worse than no check: it reads as though staleness were covered when the only thing actually covering it is the updatedAt comparison.

How it is verified

The contract passes the Pigfox Solidity pipeline on every push: forge lint and Solhint, formatting, build, tests, 100% line, statement, branch and function coverage with no exclusions, Slither at fail-on: low, and property fuzzing under both Echidna and Medusa.

Nine properties are claimed. The verdict logic is a pure function — it takes the round data and the observation time as arguments and reads neither state nor clock — which is what makes them provable at all: a live aggregator cannot be asked to go stale, answer negative, or hand back an incomplete round, so those states are reached with mock feeds built from source inside the fuzzers’ own in-process EVM.

The property count is declared, not counted: the harness states it as a literal, a static gate counts the predicates, and each fuzzer is checked against what it actually registered at runtime — so a property that quietly stops being picked up fails the build instead of reporting a smaller green run. The harness’s own detectors are tested too, by installing a checker built to break each property and requiring the matching predicate to notice, with a control that requires a faithful checker to trip nothing. Full detail, including the measured heartbeats and the live read-back, in the repository.