Every Chainlink price feed is a contract you can read directly, no SDK required, but the addresses live in a registry that changes per chain and per pair. Get one wrong and you’re reading a stale feed, a deprecated aggregator, or the wrong asset entirely. This page is a checked reference for the pairs developers ask for most: ETH/USD, BTC/USD, and USDC/USD on Ethereum, Base, and BNB Smart Chain.
Chainlink feed key facts
latestRoundData()returns five fields:roundId,answer,startedAt,updatedAt, andansweredInRound.answeris the raw price; the other four exist so you can validate freshness.- Decimals are not always 8. USD-quoted pairs on the chains below all use 8, but some crypto-quoted pairs elsewhere use 18. Call
decimals()and scale with it, don’t hardcode1e8. - Staleness must be checked against
updatedAt, never assumed. CompareupdatedAtto the current time and to the feed’s documented heartbeat before trusting the value. - Every address below was verified two ways: cross-checked against Chainlink’s own reference data directory and confirmed live on-chain by calling
decimals()anddescription()against a public RPC for each chain.
TL;DR
Chainlink price feed addresses differ per chain and per pair, and some duplicated entries in Chainlink’s own registry point at internal streams variants you shouldn’t use publicly. The tables below list the canonical proxy address for ETH/USD, BTC/USD, and USDC/USD on Ethereum, Base, and BNB Smart Chain, each independently verified on-chain.
Why the wrong address is an easy mistake
Chainlink publishes far more than one contract per pair. A single pair like ETH/USD typically has a primary proxy address plus one or two internal “shared SVR” variants used for Chainlink’s own low-latency data streams infrastructure, not intended for public latestRoundData() reads. All of them show up under the same "ETH / USD" label in Chainlink’s reference feed listing, and only one is the address that data.chain.link documents and that Chainlink’s own reference data directory files under the plain, unsuffixed feed path.
Pull the wrong one from a stale blog post, a forum answer, or a hallucinated LLM response, and you either get a revert, a feed that silently stopped updating months ago, or a decimals mismatch that scales your price 10 billion times too small. The addresses below are the canonical proxy addresses, the same ones the Chainlink price feed reader tool on this site uses.
Ethereum
| Pair | Address | Decimals | Heartbeat |
|---|---|---|---|
| ETH / USD | 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419 |
8 | 3600s (1h) |
| BTC / USD | 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c |
8 | 3600s (1h) |
| USDC / USD | 0x8fFfFfd4AfB6115b954Bd326cbe7B4BA576818f6 |
8 | 82800s (23h) |
Base
| Pair | Address | Decimals | Heartbeat |
|---|---|---|---|
| ETH / USD | 0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70 |
8 | 1200s (20min) |
| BTC / USD | 0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F |
8 | 1200s (20min) |
| USDC / USD | 0x7e860098F58bBFC8648a4311b374B1D669a2bc6B |
8 | 86400s (24h) |
BNB Smart Chain
| Pair | Address | Decimals | Heartbeat |
|---|---|---|---|
| ETH / USD | 0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e |
8 | 60s |
| BTC / USD | 0x264990fbd0A4796A3E3d8E37C4d5F87a3aCa5Ebf |
8 | 60s |
| USDC / USD | 0x51597f405303C4377E36123cBc172b13269EA163 |
8 | 900s (15min) |
Heartbeat is the maximum interval Chainlink’s node operators commit to between updates, even with no price movement. It’s the upper bound on how stale a “latest” answer can be; a feed can also update sooner if the price moves past its deviation threshold. BNB Smart Chain’s feeds above update roughly every minute; Ethereum’s USDC/USD feed only commits to once every 23 hours, which is normal for a stablecoin pair that rarely moves.
Verify before you ship
Chainlink occasionally deprecates or migrates feeds. Before hardcoding any address into production code, confirm it against data.chain.link for the pair and chain you need, the same check this reference relied on.
Checking staleness yourself
latestRoundData() gives you everything needed to decide whether an answer is trustworthy, but it doesn’t decide that for you. The pattern:
- Read
answerandupdatedAttogether, in the same call. - Compare
updatedAtto your current time. If the gap exceeds the feed’s heartbeat by a wide margin, the feed is stuck, not just running a little behind schedule. - Reject or flag the read if step 2 fails, rather than silently using a stale price.
import { createPublicClient, http } from "viem";import { mainnet } from "viem/chains";
const AGGREGATOR_ABI = [ { name: "latestRoundData", type: "function", stateMutability: "view", inputs: [], outputs: [ { name: "roundId", type: "uint80" }, { name: "answer", type: "int256" }, { name: "startedAt", type: "uint256" }, { name: "updatedAt", type: "uint256" }, { name: "answeredInRound", type: "uint80" }, ], },] as const;
const ETH_USD = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419";const HEARTBEAT_SECONDS = 3600;
const client = createPublicClient({ chain: mainnet, transport: http() });
const { answer, updatedAt } = await client.readContract({ address: ETH_USD, abi: AGGREGATOR_ABI, functionName: "latestRoundData",});
const ageSeconds = Math.floor(Date.now() / 1000) - Number(updatedAt);if (ageSeconds > HEARTBEAT_SECONDS * 2) { throw new Error(`ETH/USD feed is stale: last updated ${ageSeconds}s ago`);}Doubling the heartbeat before treating a feed as stale gives room for normal network variance without masking a genuinely stuck oracle.
Reading a feed with evmquery
The same read collapses into one REST call with evmquery, scaling answer by decimals() server-side instead of leaving that math to the client:
curl -X POST https://api.evmquery.com/api/v1/query \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "chain": "evm_ethereum", "schema": { "contracts": { "feed": { "address": "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" } } }, "expression": "cel.bind(d, feed.decimals(), cel.bind(r, feed.latestRoundData(), { \"description\": feed.description(), \"price\": string(formatUnits(r.answer, d)), \"roundId\": string(formatUnits(r.roundId, 0)), \"startedAt\": string(formatUnits(r.startedAt, 0)), \"updatedAt\": string(formatUnits(r.updatedAt, 0)), \"answeredInRound\": string(formatUnits(r.answeredInRound, 0)) }))" }'This is the same expression shape validated live against the ETH/USD feed above; it resolves to a map with the pair description, the human-readable price, and all four round-integrity fields as strings, in one round trip and four billed units total: one for the CEL evaluation plus three calls for decimals(), latestRoundData(), and description(). The Chainlink price feed reader tool on this site runs a simpler version of this expression (just description, scaled answer, and decimals) if you want to try a feed before writing any code.
If you’re pulling more than one pair at a time, for example a treasury dashboard reading ETH/USD, BTC/USD, and USDC/USD in the same request, each additional feed just adds another entry to schema.contracts and another key to the expression’s output map. The Multicall3 guide covers the batching mechanics if you’re building this by hand instead of through evmquery.
When you’d automate this
A one-off price check rarely needs staleness handling this careful, but a scheduled job does. If you’re building a price alert, a liquidation monitor, or a treasury dashboard that polls Chainlink on a timer, see evmquery for automation for wiring the same expression into a scheduled workflow instead of a script you have to babysit.
Next steps
- Chainlink price feed reader tool: try any of the addresses above against a live query, no code required.
- Multicall3 batching guide: read multiple feeds, or a feed alongside other contract state, in a single call.
- evmquery for automation: wire a feed read into a scheduled monitor or alert.



