Editorial draft · not published
How to Find the Ethereum Block Number for a Date
Resolve a UTC cutoff to an Ethereum block using timestamp boundaries and binary search, with explicit edge cases, caching rules and verification steps.

Overview
When translating these concepts into an integration, keep the scope described by the wallet history api with the result. The examples in wallet transaction history and smart contract event logs show how the same distinction affects neighbouring data questions.
A date-to-block lookup is a boundary search. It should answer a precise question: which observed block is the last one whose timestamp is at or before a UTC instant? An average block-time estimate can provide a starting guess, but it cannot demonstrate that boundary. A reporting system should retain the selected block and its successor rather than merely return a number.
This guide explains a binary search over a fixed chain view, how to handle unsupported dates and how to cache the result without confusing a provisional observation with a permanent fact. The selected block can then become the input to a historical balance query.
Start with a UTC instant and a selection rule
Require a complete date and time, including an explicit zone. A date such as 2025-12-31 alone leaves unanswered whether the user means the start of the day, the end of the day or a local reporting cutoff. Normalize to UTC only after deciding that policy. Keep the original input and its interpretation available for support and audit review.
The rule used here is floor by timestamp: choose the greatest height whose block timestamp is less than or equal to the target. A ceiling rule would answer a different question. Neither rule creates a block at the exact requested instant. Explain the difference in the UI and retain the selected timestamp so users can see the small gap between their cutoff and the nearest eligible observation.
Freeze the upper boundary before searching
Read a suitable upper block and retain its number, hash and timestamp. Search within that fixed numeric interval rather than asking latest again on every iteration. Otherwise the search range can move while the algorithm is running, making debugging and reproducibility harder even if the final height happens to be plausible.
For a final reporting boundary, the application needs a finality policy appropriate to the network. This guide does not prescribe a universal confirmation count. A recent block near the head should be identified as provisional under your policy. Store the head context with the search result so a later check can determine whether the selected boundary was replaced or whether the original query simply used an older view of the chain.
Use binary search for the floor boundary
At each step, request the midpoint block and compare its timestamp with the target. If the timestamp is not later than the target, remember it as a candidate and move the lower bound above it. Otherwise move the upper bound below it. The interval shrinks until the best eligible height remains. This approach avoids scanning every intervening block.
The algorithm assumes a correctly identified chain and an ordered timestamp sequence in the searched canonical view. Validate each returned block number and timestamp rather than trusting a response at an unexpected height. A missing midpoint is an error or unavailable observation, not permission to skip an arbitrary interval. Quietly skipping failed reads can turn a deterministic boundary search into an undocumented approximation.
Verify the selected block and its successor
After the search, read the candidate and the next block where it exists in the fixed view. Check candidate.timestamp ≤ target and successor.timestamp > target. Retain their hashes and the successor’s parent hash. These observations explain the boundary and can also expose an inconsistent response set during a chain replacement or endpoint synchronization problem.
If there is no successor within the frozen view, the result is an upper-bound observation rather than a fully bracketed historical cutoff. Do not pretend that a future timestamp maps definitively to the current head. Return an explicit out-of-range or provisional condition. A report should be able to distinguish a genuine floor boundary inside known history from a target later than the latest available observation.
Handle dates outside the available interval
A target before the first available block has no eligible result under the floor rule. A target after the frozen head is beyond the searched interval. These conditions should not both return the same convenient sentinel number. Keep the reason explicit so the calling application can ask for a different range or explain why its requested cutoff cannot yet be answered.
Historical block headers and historical state are different capabilities. Finding a block number successfully does not prove that a later eth_call can read token state at that height. Preserve that separation in integration tests and in the user interface. A date resolver can succeed while a balance observation remains unavailable, and the final response should not disguise that distinction as an empty or zero balance.
Cache the query and the evidence together
A cache key should include chain identity, the exact UTC target and the selection policy. Store the candidate, successor and observation time. Caching only a date string without a time zone or policy can merge different reporting questions. Caching only the returned height loses the evidence needed to verify why that height was chosen.
For finalized historical boundaries, the application may choose a long retention period. For recent provisional boundaries, revalidation is necessary under the chosen finality policy. A cache hit is not an independent proof of canonicality. If a revalidation finds a changed block hash, preserve the previous result as a replaced observation and identify which downstream snapshots or reports used it.
Test the boundary rather than an estimated height
Use a small synthetic sequence with known timestamps. Test a target exactly equal to a block timestamp, a target between two blocks, one before the first block and one after the frozen head. Assert the floor relationship and successor relationship, not just that the returned height is numerically close to an estimate.
Add malformed responses, a wrong chain ID, a missing block and a changed parent relationship. Keep the RPC request budget bounded and expose a cancellation path. A date resolver should not issue an unlimited sequence of calls when an endpoint behaves inconsistently. A deterministic test can count requests and prove that each successful midpoint read reduces the interval, while a failed read stops with an explicit explanation.
Walk through a small deterministic chain
Imagine five synthetic blocks with increasing timestamps: 100, 112, 127, 141 and 154 seconds. A target of 130 should select the third block, while 127 should also select the third under the floor rule. A target of 99 has no eligible result, and a target of 160 lies beyond the frozen upper view. These small cases make the policy visible without relying on a real network or estimating an expected height from a calendar.
Now change the third block’s identity while leaving its height unchanged. The selected number can remain correct while the evidence changed. A result model that stores only the height cannot explain that difference. Retain the hash and successor relationship so the application can distinguish a stable historical boundary from a revised chain view. This is particularly relevant when the target is close to the observed head.
Count the adapter calls in the test. Each successful midpoint read must shrink the remaining interval. A missing block should stop the search under a documented error, not cause an arbitrary jump. After finding the candidate, the successor verification is a separate acceptance step. Testing those properties establishes the algorithm’s meaning more clearly than timing one fast query against a live endpoint.
Design the result for a reporting consumer
Return the original UTC instant, selected block number, selected timestamp and verification status. Include whether the successor bracket was established within the frozen view. A consumer that only needs the height can display it compactly, but the underlying record should keep the supporting context. That allows a later balance lookup to explain exactly which state boundary it used.
Use different outcomes for before-history, after-view, missing-header and inconsistent-view conditions. They imply different next actions. A user can correct an unsupported date, wait for a future cutoff to become observable or retry a temporary header failure. Collapsing them into block zero or a generic success makes the UI simpler only by moving the ambiguity into downstream reports.
When a caller requests multiple dates, deduplicate identical scope queries and cache verified historical brackets. Do not let cache reuse erase chain identity or policy version. A fiscal-period boundary selected under a floor rule is not interchangeable with one selected under a ceiling rule, even if both occasionally return the same height. Record the rule so equality of outputs is not mistaken for equality of questions.
Worked implementation example
async function floorBlock(targetSeconds, high, getBlock) {
let low = 0n, best = null;
while (low <= high) {
const mid = (low + high) / 2n;
const block = await getBlock(mid);
if (!block || BigInt(block.number) !== mid) throw Error('Missing block');
if (BigInt(block.timestamp) <= targetSeconds) {
best = block; low = mid + 1n;
} else high = mid - 1n;
}
return best;
}The getBlock adapter is deliberately injected so the algorithm can be tested with synthetic responses. It should send eth_getBlockByNumber using a hexadecimal quantity and request only the header context needed for the search. The caller freezes high, validates the chain and verifies the final successor boundary. The code does not make those application policies disappear.
Keep timeout, cancellation and request-count limits outside the mathematical loop but inside the adapter contract. A report should retain the target instant, selected block and successor even when it only displays a compact block number to the user. That small evidence record is more useful than a claim that the result was computed with a fast algorithm.
Frequently asked questions
Why not divide elapsed time by average block time?
An average is useful for an estimate, not an exact boundary. Validate the actual timestamps around the target before using a height in a report.
Which block should midnight map to?
Define the policy first. Under a floor rule, choose the last observed block at or before midnight UTC and retain the next block to demonstrate the boundary.
Does finding the block guarantee historical token state?
No. Header retrieval and historical state retention are separate capabilities. A later balance call can still fail and must remain an explicit gap.
References and further reading
Continue with historical token balances and rpc vs indexed data api. Product scope: historical balance api.