Balance Guards and Derived-Address Validation
Every EVM bot must derive its EOA from the configured private key, log it at startup, and compare it to an expected-address env var. The Hermes wallet mismatch cost an hour of debugging — a five-line startup check would have caught it in one.
The Hermes wallet mismatch was one of the cleanest silent failures in the session. The bot reported "USDC balance: $0.00". The user had funded the wallet with $14.99 minutes earlier. Both were true — and also irrelevant — because the bot was checking the balance of a different address than the one that had been funded.
The Bug in 30 Seconds
Doppler held a POLYMARKET_PRIVATE_KEY environment variable. Hermes loaded it at startup and used it to sign orders. The address that Hermes actually derived from that key was 0x081c…0115. The address the user had funded was 0xCF56…0aD. Two completely different EOAs — because the Doppler secret had drifted from the intended key during a prior rotation and nobody had propagated the change to Hermes.
The diagnostic hour was burned on wallet-funding debugging before anyone thought to check whether Hermes was looking at the right wallet at all. A screenshot from the user's Metamask showing the funded address broke the case open in 30 seconds — the addresses didn't match.
The Fix: Startup Derivation Check
Five lines of code, added to every EVM bot in the ecosystem:
from eth_account import Account
def verify_wallet_at_startup() -> None:
private_key = os.environ["POLYMARKET_PRIVATE_KEY"]
expected_wallet = os.environ["EXPECTED_WALLET"]
derived = Account.from_key(private_key).address
logger.info(f"bot_startup derived_wallet={derived} expected={expected_wallet}")
if derived.lower() != expected_wallet.lower():
logger.error(f"WALLET_MISMATCH derived={derived} expected={expected_wallet}")
raise SystemExit(1)
This runs before any network call. It catches:
- Key rotation drift — Doppler updated but the bot has the old key cached.
- Wrong-environment deploys — a dev key accidentally deployed to prod.
- Secrets manager misconfiguration — the key you pulled is not the key you expected.
- Typos — a bad letter in the env var name resolved to a blank string.
Any of those produce a fast, loud failure at startup. No hour of balance debugging. No silent running on the wrong wallet.
The Balance Guard Extension
Once the derivation check passes, the next guard is the balance check. It needs to query two balances and confirm they agree:
on_chain = usdc_e_contract.balanceOf(derived)
polymarket_reported = clob.get_balance_allowance(asset=USDC_E)["balance"]
assert on_chain > 0, f"USDC.e balance is zero at {derived}"
assert on_chain == polymarket_reported, f"On-chain {on_chain} != Polymarket {polymarket_reported}"
logger.info(f"balance_verified wallet={derived} usdc_e={on_chain}")
This catches a separate failure mode: the wallet is correct, but the allowance has not been set for the Polymarket exchange contract. On-chain balance and Polymarket-reported balance will diverge. Without this guard, you only find out when the first order is rejected for insufficient allowance.
Inline Diagram — Startup Check Sequence
The Rule
Hermes had this pattern added to its startup sequence as part of the session retro. The same pattern is being back-ported to Foresight, Shiva, and Leverage. The prevention is ten minutes per bot. The cost of skipping it is measured in hours every time a key rotates and something goes silent.