In Chainlink's documented EACAggregatorProxy-style design, a proxy round ID combines a 16-bit phase ID with a 64-bit aggregator round ID. The phase selects an aggregator in that proxy's history; the local round selects a report within it. Decode both parts before comparing or requesting historical rounds, and preserve the full ID as an exact integer.
One feed address can lead to different aggregators
A consumer usually reads through the feed's proxy address. That address can continue serving the feed while its underlying aggregator changes. Chainlink describes EACAggregatorProxy as a common example and explicitly notes that proxy contracts can differ between feeds. The encoding below covers that documented design, rather than every contract exposing a similar interface. Chainlink's proxy architecture.
For a precise reference, this article uses EACAggregatorProxy and its AggregatorProxy parent from the official contracts-v1.0.0 tag, written for Solidity 0.6.6. The parent maintains a current phase and a phase-to-aggregator mapping. Its latestRoundData() call adds the active phase to the aggregator's returned identifier; its getRoundData() call decodes the requested phase and routes to the corresponding aggregator. Versioned AggregatorProxy source.
Think of the phase as an archive label and the aggregator round as a record number inside that archive. Record 27 in one archive differs from record 27 in another. Neither the local number alone nor the proxy address alone fully identifies the report.
The complete identity also needs the network. A useful stored key combines network, proxy address and full proxy round ID. Keeping the resolved aggregator address makes later investigation easier. The broader distinction between a stable address and changing behavior appears in Ethereum code hashes versus contract addresses.
Match the requested ID to the address receiving the call. For this reference implementation, a historical read through the proxy takes the full encoded ID. The proxy passes only the local part to the phase's aggregator. A direct aggregator read, when its interface and access policy permit it, therefore takes the aggregator's own ID. Passing local round 28 to the proxy instead decodes as phase zero, local round 28; it does not ask for phase 3's report. The address and identifier must come from the same namespace.
Decode the integer without rounding it
Let M equal 2 to the power of 64, or 18446744073709551616. For this encoding, the proxy ID is the phase multiplied by M, plus the local round. Equivalently, it is the phase shifted left by 64 bits, combined with the local round using bitwise OR. Chainlink's historical-data guide documents this split. Proxy round-ID construction.
The operations can be checked separately:
| Operation | Exact integer rule |
|---|---|
| Encode | Multiply phase by M, then add local round |
| Decode phase | Integer quotient when dividing proxy ID by M |
| Decode local round | Remainder when dividing proxy ID by M |
| Check reconstruction | Decoded phase times M, plus decoded local round, must equal the original |
The representable fields are 16 bits for the phase and 64 bits for the local round. A value fitting those fields is only well-formed arithmetic. It does not establish that a phase is configured or that an answer exists for the requested round.
For an illustrative input, phase 3 and local round 27 encode as 55340232221128654875. Dividing that integer by M gives quotient 3 and remainder 27. The same local round in phase 4 would identify a different namespace. The prefix is part of the report's identity, not a decimal scale applied to the answer.
Preserve these IDs as exact integers throughout decoding, storage and transport. JavaScript's Number type guarantees distinct integer representation only through 9007199254740991, far below these examples. Use an appropriate arbitrary-precision integer representation and serialize exact decimal strings where needed. ECMAScript's safe-integer definition.
Converting an already rounded Number to BigInt cannot restore lost digits. In the example below, taking 73786976294838206474 through Number and then back to BigInt produces 73786976294838206464: local round 10 has become local round zero. Parsing the original decimal string directly avoids that loss.
Follow an upgrade through four snapshots
The following sequence is entirely illustrative. P is one hypothetical proxy using the cited implementation. Aggregator A is active in phase 3. A replacement, B, already produces reports before it becomes active at P. B0 through B3 are successive block-end snapshots; all reads within a row use that snapshot. Assume A's round 28 remains its latest report until the switch.
In the reference source, proposing an aggregator records the candidate. Confirming the proposed address activates it and increments the phase. Proposal alone does not switch the latest-data read. Proposal and confirmation in the reference contract.
| Snapshot | Change by this snapshot | Active aggregator | Phase / local round | Proxy round ID |
|---|---|---|---|---|
| B0 | Initial observation | A | 3 / 27 | 55340232221128654875 |
| B1 | B proposed; A has another report | A | 3 / 28 | 55340232221128654876 |
| B2 | B confirmed; its latest report is 10 | B | 4 / 10 | 73786976294838206474 |
| B3 | B has another report | B | 4 / 11 | 73786976294838206475 |
Between B0 and B1, the ID increases by one. Between B1 and B2, it increases by 18446744073709551598. That large jump equals M minus 18: the phase contribution increases by M while the local component falls from 28 to 10. It is a namespace transition, not a count of that many intervening reports.
At B2, the local round number is smaller, yet the full proxy ID is larger. An alert comparing only local numbers could incorrectly report that the feed moved backward. Conversely, an alert treating every full-ID gap as missing reports would invent a huge data outage.
The example deliberately activates B at local round 10. An upgrade does not require the replacement's latest stored report to be its first round. An importer that always begins a newly active phase at local round one needs evidence that this starting point matches the history it intends to collect.
A previous stored round may precede activation
Suppose B also holds local round 9, produced before confirmation. At B2, subtracting one from the full latest ID gives 73786976294838206473. This decodes to phase 4, local round 9. If that report is available through the phase mapping, the request may succeed.
It still does not identify the last answer P served while A was active. In the constructed timeline, that was phase 3, local round 28. The two questions require different evidence:
- To retrieve an aggregator's stored report, identify its phase, a valid local round and the requested snapshot.
- To reconstruct what the proxy exposed over time, establish the activation boundary and the reports available at each relevant block.
This distinction matters when a chart or retrospective calculation claims to show the feed a consumer could actually read. A replacement aggregator's earlier reports can be real stored observations while falling outside its period as the proxy's active source. Label which history the dataset represents.
Even the arithmetic boundary does not cross phases automatically. In a separate illustrative case, subtracting one from phase 4, local round 1 produces phase 4, local round zero. It does not discover the final valid report in phase 3.
Chainlink's historical guide includes off-chain iteration examples, but its consumer example also warns that round IDs are not all valid. Treat round discovery as implementation-specific work. A failed request alone cannot prove that all earlier or later history has been exhausted. Retain the failure and distinguish a contract error from unavailable historical RPC state or a transport failure. Historical-round guidance and validity warning.
Build a receipt before building a history loop
Start with one known report and make it reproducible. Bind the reads to a chosen block, rather than combining several changing latest states. The RPC batch versus block-snapshot guide explains why putting requests in one batch does not itself freeze their state.
Use this worksheet for each phase boundary you investigate:
| Field | Evidence to retain |
|---|---|
| Feed identity | Network, full proxy address, official address reference and intended data pair |
| Snapshot | Block number and hash, provider, observation time and historical-state availability |
| Routing | Proxy design/source match, active phase, active aggregator and relevant phase mapping |
| Report | Exact requested and returned IDs, decoded phase/local round, raw answer and timestamps |
| Activation | Evidence of when the replacement became active, with uncertainty recorded |
| Interpretation | Stored aggregator history or history of proxy exposure |
| Failure | Raw error, request context and unresolved cause; never a fabricated zero-valued report |
For the worked example, validate that B1 still resolves to A, B2 resolves to B, and both returned IDs reconstruct exactly. Then check whether a B9 request answers the question the dataset is meant to answer. A successful read can still be the wrong historical observation for that purpose.
Before widening retrieval, inspect the actual deployed proxy and aggregator. Chainlink recommends checking their source and configuration; the aggregator's typeAndVersion identifies its type and implementation version, while version is a separate value. Similar interfaces do not justify identical assumptions about their internals. Aggregator identification in the API reference.
The operating model needs verification too. Chainlink documents self-managed deployments with Data Feed-style proxies whose updates come from a chain or third-party operator. Interface similarity alone does not establish who operates or monitors them. Self-managed feed distinction.
Keep report identity separate from price validation
Decoding a round identifies a report; it does not establish freshness or acceptable price quality. Preserve the returned tuple, then apply the consumer's policy. Chainlink's current API reference marks answeredInRound as deprecated, so an old comparison involving that field should not become a universal validation rule copied into every integration. Current round-data return fields.
The oracle staleness and deviation guide handles report age and update triggers. Price units need their own check: feed decimals versus token decimals separates answer precision from token quantity precision. If combining two feeds, cross-rate scaling adds the unit and timestamp alignment that a correct round ID cannot supply.
For the next historical read, save three identities together: the proxy on its network, the phase's aggregator, and the exact local round. Add the block and activation evidence whenever the question is what a consumer could read at that time. That record makes an upgrade boundary explainable without inventing missing reports or silently changing the meaning of the history.
