ASK KNOX
beta
LESSON 267

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 Agent Framework wallet mismatch cost an hour of debugging — a five-line startup check would have caught it in one.

7 min read·Prediction Market Mechanics

The Agent Framework wallet mismatch was one of the cleanest silent failures in the debugging incident. 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 (a secrets manager) held a POLYMARKET_PRIVATE_KEY environment variable. Agent Framework loaded it at startup and used it to sign orders. The address that Agent Framework 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 Agent Framework.

The diagnostic hour was burned on wallet-funding debugging before anyone thought to check whether Agent Framework 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:

USDC_DECIMALS = 10 ** 6  # USDC.e is 6-decimal; balanceOf() returns raw uint256
raw_on_chain = usdc_e_contract.balanceOf(derived)  # e.g. 14990000 = $14.99
on_chain_float = raw_on_chain / USDC_DECIMALS
polymarket_reported = float(clob.get_balance_allowance(asset=USDC_E)["balance"])

assert on_chain_float > 0, f"USDC.e balance is zero at {derived}"
assert abs(on_chain_float - polymarket_reported) < 0.01, (
    f"On-chain {on_chain_float:.2f} != Polymarket {polymarket_reported:.2f}"
)
logger.info(f"balance_verified wallet={derived} usdc_e={on_chain_float:.2f}")

This catches a separate failure mode: the wallet is correct, but the allowance has not been set for the Polymarket exchange contract. An allowance is the ERC-20 spending permission you grant a specific contract with an approve() transaction — until it is set, the exchange can see your USDC.e but has no right to move it, so the on-chain balance and the Polymarket-reported balance will diverge. Without this guard, you only find out when the first order is rejected for insufficient allowance. The "Dual-Approval Wall" lesson at the end of this track covers the approval flow in full.

Inline Diagram — Startup Check Sequence

The Rule

Agent Framework had this pattern added to its startup sequence as part of the incident retro. The same pattern is being back-ported to Foresight, the sports bot, and the perp bot. 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.