Am0MuKI wrote a small open-source tool, onchain-tieout, to answer one question: does the balance you...
I wrote a small open-source tool, onchain-tieout, to answer one question: does the balance you reconstruct from a wallet's transaction history match the balance the chain actually reports, at the same block?
That check exists because data pipelines fail silently. An explorer API returns HTTP 200 with an error inside the body, a page of results is capped without notice, a transaction that reverted still shows a non-zero value. Everything "succeeds", and the number is wrong.
The first implementation came from an AI coding agent working from my specification. The offline test suite was green. Then I pointed the tool at a real mainnet wallet, and the first things it found were bugs in itself.
1. The page size I specified was wrong, and every test agreed with me
Etherscan's documentation describes a 10,000-row result window, and I had written about that cap myself. So the client requested offset=10000 and treated any page shorter than that as the last one.
In reality, Etherscan V2 returned 1,000 rows per page, even when 10,000 were requested. The client saw 1,000 < 10,000, concluded it had reached the end of the history, and stopped. That is silent truncation, the failure class the tool exists to catch.
The tie-out itself caught it: the reconstructed ETH balance was far off the on-chain balance at the same block. The tool did its job on its own code.
The tests missed it because every offline test used a mocked API, and the mocks were written from the same wrong assumption as the code. A mock can only confirm what you already believe about the system behind it.
2. A single block with more than one page of rows
The standard way to paginate an explorer is by block: take the last block of a full page, drop its rows, and request again from that block, so the boundary block is fetched whole.
The real wallet had more than 1,000 token transfers in one block (a spam airdrop). A block-based cursor cannot move past a block that alone fills a whole page. The fix reads such a block on its own with page numbers, up to the API's 10,000-row window, and fails loudly beyond that instead of guessing. Before the fix, that wallet's balances came out wrong.
3. Rate limits reported as data
The next run marked about 9,700 tokens as "balance could not be read". That label is meant for spam or non-standard token contracts whose balanceOf call reverts.
Sampling the failures showed most of them were HTTP 429 responses from the RPC provider. The tool was turning "the provider is busy" into "this token is unreadable", a network problem reported as a fact about the data.
The fix separates the two. Transport problems (429, 5xx, timeouts) are retried with back-off and, if they persist, abort the run. Only a real revert or an empty result is reported as an unreadable balance.
4. Two bugs found by reading, not running
Reading the agent's code line by line turned up two more:
status: "1" but a result that was not a list was silently turned into an empty history.Both have the same shape as the bugs above: an ambiguous input turned into a plausible answer.
5. What "complete" looks like on a real wallet
With the fixes in place, the full run on that wallet (vitalik.eth) produced 10,476 balance rows. 8,281 tied out exactly. Almost all of the rest were airdropped spam: contracts that emit Transfer events inconsistent with their own balanceOf, many reusing real symbols such as USDC or WETH on other addresses. 175 of them had a history in which the wallet sent more than it ever received, impossible for a standard ERC-20, now labelled negative_history.
On the assets that matter, the result is clean. ETH, DAI, USDC and USDT tie out to the last unit. stETH differs because it rebases without transfers. The WETH difference equals the wallet's own deposit() minus withdraw() calls exactly, because WETH wraps emit no Transfer event.
One more silent failure turned up in the output formatting: Python's Decimal rounds to 28 significant digits by default, so very large spam amounts were printed rounded and tiny ones as 2.2E-7. Amounts are now formatted with integer arithmetic only.
What I took from it
Offline tests prove the code matches your assumptions. Only a run against the real system tests the assumptions. A reconciliation check is also worth running on the reconciliation tool itself: the first live tie-out failed for the right reason, and that failure was the most useful test result of the project.
"Could not read" and "does not exist" are different answers, and a tool that merges them will eventually report an outage as a fact about your data. When the input is ambiguous, fail loudly. Every bug here was an ambiguous case resolved into a plausible number.
Getting from the first live run to a clean tie-out took days. I would let an AI agent write the first version of a tool like this again.
The tool is open source (Python, MIT): github.com/Am0MuK/onchain-tieout
This describes software behavior, not tax or financial advice.