I keep a dashboard that reads my own wallets across a couple dozen chains. It holds no keys and cannot sign anything. The only thing it does is tell me what I have.
Most chains make that easy. There is a free explorer API that will list every token an address holds, so the list is discovered rather than declared.
BNB Chain does not, at least not without an API key I would rather not depend on. So token discovery there cannot be automatic. What is left is probing: take a list of contracts you think matter, call balanceOf on each one, keep whatever comes back non-zero.
Which means writing the contracts down. Fourteen of them, each a bridged claim on some other chain's coin, each with a symbol and a decimals count next to it.
Every one of those hex strings was written from memory. That is the part worth a post.
Three ways a remembered address goes wrong
It points at nothing. The good outcome. balanceOf returns zero, the row never appears, and I am merely missing a holding I have. Annoying, visible, fixable.
It points at something. A remembered address is not a typo. It is a real address I have seen before, possibly a real token, possibly one I actually hold. So the dashboard shows a balance, read correctly off the chain, under the name of a different asset and priced as that different asset. A number that is right about everything except what it is.
The decimals are wrong. Most tokens use 18. Some use 8. Write 18 where the contract says 8 and every quantity is off by a factor of ten billion. Write 8 where it says 18 and a real position shows up as a rounding error.
The chain id has the same problem with a sharper edge. It is the number that says "this is BNB Chain, this is Polygon," and it is what a send link gets built from. A wrong chain id does not produce a wrong number on a screen. It produces a wallet prompt pointed at the wrong network.
You cannot proofread hex
Here is the actual difficulty, and it took me a minute to say it plainly.
A wrong address and a right address are the same object. Both are forty characters of hex. Neither is misspelled, because there is no spelling. Reading the table tells you nothing, and a second person reading the table tells you nothing, because the only way to check a hex string is to go look it up somewhere — at which point you are not reviewing anymore, you are verifying, and the program may as well do it instead of you.
That is true of more than token addresses. An endpoint path, a magic number, a field name in someone else's schema. Constants written from memory rather than derived are assertions about the world that look, sitting in a source file, exactly like facts.
The contract knows its own name
Every ERC-20 exposes symbol() and decimals(). Every RPC endpoint answers eth_chainId. No keys, no account, nothing but a round trip.
symbol() 0x95d89b41
decimals() 0x313ce567
So now the name gets read off the contract before a balance is shown under it. An address that is not a responsive token returns nothing decodable, which is exactly what a wrong address looks like from the outside. That is no longer a guess about which of the three failure modes I am in. It is an answer.
Quantities convert using the decimals the contract reports, never the decimals in my table. That does not fix the factor-of-ten-billion bug. It removes the category.
Most of the decoder's job is saying no
symbol() returns a string, and there are two conventions for that. The modern one is a 32-byte offset, then a length, then the bytes. The older one, from tokens deployed before that settled, is a raw bytes32 — the ASCII, right-padded with zeros. You handle both.
More importantly, you handle everything that is neither. An address with no code returns 0x. A contract that is not a token returns whatever its fallback function felt like. A reverting call returns an error. None of those can be allowed to become a plausible-looking ticker, because a plausible-looking ticker is the exact failure I was trying to prevent.
So the tests that matter are not "does it decode USDT." It does. They are "does thirty-two bytes of 0xff come back as nothing," and "does a length field claiming four thousand characters come back as nothing." Nine cases, and seven of them are about refusing to answer.
Which direction to fail
When the chain disagrees with my table, the app has to do something, and I ended up with two opposite answers.
For a token, deleting the row is wrong. The complaint that started all of this was a dashboard that understated what I actually hold, and quietly dropping a real balance because a label failed a check makes that worse. So the balance stays on screen, under the symbol the contract actually reports, marked unverified, with a note saying what disagreed, and priced by contract address instead of by the label that turned out to be wrong. The money stays visible. Only my confident name for it loses its authority.
For a chain id, the opposite. A confirmed mismatch strips the id out of every position on that chain. Balances still render, and the send form says unknown chain id and refuses to build a link at all.
The rule underneath both: fail toward visibility when being wrong costs you a missing number, and fail toward refusal when being wrong costs you an irreversible action.
What it costs
Two extra calls per token that actually has a balance, one per chain for the id. Thirty-day cache on the token metadata, seven-day on the chain ids, live read when the cache is down. Steady state is nothing. Cheap, for turning fourteen assertions into fourteen answers.
What it does not fix
Verification confirms a contract is what it says it is. It does not confirm it is the one I meant.
Anyone can deploy a token whose symbol() returns USDT with eighteen decimals. It passes every check above, because it is not lying about itself — it is a real token that really is named that. What this catches is my table being wrong: mistyped, stale, misremembered. What it does not catch is a well-made impostor, and that needs something else entirely, like a curated list or a cross-check against whether a price source recognises the contract at all.
The dashboard does flag tokens whose names impersonate a chain's native coin, because a token calling itself ETC on a chain where ETC is the native coin is almost always a scam airdrop. That is a heuristic and not a proof, and it is worth being clear about which one you have.
The general version of this is the same thing I wrote about self-healing scrapers a couple of days ago. There, a selector the model suggested was accepted only if reading it actually reproduced the value the model claimed to find. Same idea here. A value produced from memory is a hypothesis, and the fix is never to review the hypothesis harder. It is to find the thing that already knows the answer and ask it.