An ERC-4337 smart account doesn’t keep its own nonce the way an EOA does, and it doesn’t hold a local balance earmarked for gas either. Both live in one singleton contract, the EntryPoint, shared by every smart account on the chain. Wondering whether your account’s last UserOperation actually landed, or whether a paymaster still has funds to sponsor the next one, means reading someone else’s contract — not your own.
TL;DR
An ERC-4337 account’s nonce and gas deposit both live on the EntryPoint singleton, not the account itself. getNonce(sender, key) and getDepositInfo(account) are plain view calls — no bundler SDK, no UserOperation construction needed to just read the state. The one trap: nonces are two-dimensional, so checking key=0 can silently show zero for an account that has transacted plenty under a different key.
Why account state lives outside the account
A regular EOA’s nonce is a protocol-level counter Ethereum itself tracks per address. An ERC-4337 smart account is just a contract, and Solidity contracts don’t get that counter for free — so the EntryPoint contract maintains it instead, keyed by sender address. The same EntryPoint also holds every account’s (and every paymaster’s) prepaid ETH deposit, spent down as bundlers submit UserOperations and refunded on withdrawal.
There are two EntryPoint versions in production right now, and both resolve identically across every chain evmquery supports — Ethereum, Base, Polygon, and BNB Chain — because they’re deployed via the same deterministic factory at every address:
| Version | Address | Notes |
|---|---|---|
| v0.6 | 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789 |
Still targeted by some older wallet SDKs |
| v0.7 | 0x0000000071727De22E5E9d8BAf0edAc6f37da032 |
Current default for most new integrations |
Both expose the same three read methods this guide uses: getNonce, getDepositInfo, and balanceOf. If you don’t know which version a given wallet or bundler targets, query both — an address that isn’t a valid EntryPoint on that chain fails contract resolution immediately, so there’s no ambiguity in the result.
The deposit isn’t just accounting trivia, either. Before a bundler will include a UserOperation, it simulates the operation and checks that the sender (or its paymaster) has enough deposit sitting in the EntryPoint to cover the worst-case gas cost — the “prefund” requirement. Run out of deposit and every subsequent UserOperation from that account or paymaster starts failing simulation, silently, with no on-chain transaction to point at. Reading getDepositInfo before it hits zero is the whole difference between catching a funding problem and debugging a pile of user complaints about “stuck” transactions that never made it past the mempool.
Reading nonce and deposit in one call
getDepositInfo(account) returns a struct with everything the EntryPoint knows about an address’s prepaid balance and bundler stake; getNonce(sender, key) returns the current nonce for a given 192-bit key (more on that key in a second). Both are plain view functions — bind them together with cel.bind and pull both in a single round trip:
import osimport requests
ENTRYPOINT_V07 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"
resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_base", "schema": { "contracts": {"entrypoint": {"address": ENTRYPOINT_V07}}, "context": {"account": "sol_address"}, }, "context": {"account": "0xYourSmartAccountOrPaymaster"}, "expression": ( 'cel.bind(info, entrypoint.getDepositInfo(account), {' ' "depositEth": dyn(formatUnits(info.deposit, 18)),' ' "staked": dyn(info.staked),' ' "stakeEth": dyn(formatUnits(info.stake, 18)),' ' "unstakeDelaySec": dyn(info.unstakeDelaySec),' ' "nonceKeyZero": dyn(entrypoint.getNonce(account, solInt("0")))' " })" ), }, timeout=10,)resp.raise_for_status()print(resp.json()["result"]["value"])Run live against Circle’s USDC gas paymaster on Base (0x6C973eBe80dCD8660841D4356bf15c32460271C9, a real, publicly documented paymaster contract — not a random address), this is the actual response at block 51,587,266:
{ "depositEth": 0.03875192752118992, "staked": true, "stakeEth": 0.25, "unstakeDelaySec": { "value": "86400" }, "nonceKeyZero": { "value": "0" }}depositEth and stakeEth come back as plain numbers because formatUnits returns a double; unstakeDelaySec and nonceKeyZero are raw sol_int values, and a sol_int nested inside a map<string, dyn> (the shape cel.bind’s object literal produces here) serializes as {"value": "..."} instead of a bare number or string. Read .value for those two fields, not the field directly — a bare sol_int or one inside a list<sol_int> would print as a plain string instead, so the container matters, not just the type.
depositEth is the spendable prepaid balance the paymaster draws down to sponsor other people’s gas — separate from stakeEth, which is collateral bundlers require before they’ll trust a paymaster not to grief them, locked for unstakeDelaySec (one day here) once the paymaster asks to unstake. Query the same address on evm_polygon instead and every field reads zero — Circle’s paymaster is deployed at the same address on both chains, but only funded on the ones it’s actively used on. Zero deposit means “not funded on this chain,” not “broken query.”
One MCP shape quirk to know about
The REST body above uses {"address": "0x..."} for each contract, the documented and preferred convention. If you’re validating the same query with the execute_query MCP tool during development, it wants a bare address string instead — {"entrypoint": "0x0000...da032"} — and throws a Zod error on the object form. Same product, two surfaces, different expected shape.
The nonce key trap: why key=0 can lie to you
ERC-4337 nonces aren’t a single incrementing counter — they’re two-dimensional. The full uint256 nonce packs a 192-bit key in the high bits and a 64-bit sequence in the low bits, and an account can maintain any number of independent sequences in parallel by using different keys. getNonce(sender, 0) only reads the sequence under key 0; it says nothing about activity under any other key.
This isn’t a theoretical edge case. Pulling a real UserOperationEvent from Base’s EntryPoint v0.7 logs turned up sender 0x4c64e09af5b5d6404100da1a49f5feb08670dd40, which had just executed a successful UserOperation — confirmed straight from the event log. Querying entrypoint.getNonce(account, solInt("0")) against that exact account returns:
{ "result": { "value": "0", "type": "sol_int" } }A bare, top-level sol_int result comes back as a plain string in result.value — no {"value": ...} double-wrapping here, that only happens for a sol_int nested inside a struct or map<string, dyn> field like the previous section’s response. Either way: zero. Nothing wrong with the query — the account simply transacted under key 405859755098880090987380362720584009 (the high 192 bits of the nonce the event actually emitted, extracted with nonce >> 64), not key 0. Querying getNonce with that specific key instead returns the real, incremented value:
resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_base", "schema": { "contracts": {"entrypoint": {"address": ENTRYPOINT_V07}}, "context": {"account": "sol_address", "key": "sol_int"}, }, "context": { "account": "0x4c64e09af5b5d6404100da1a49f5feb08670dd40", "key": "405859755098880090987380362720584009", }, "expression": "entrypoint.getNonce(account, key)", }, timeout=10,)print(resp.json()["result"]["value"]) # "7486791032127476292458934032162845995094385197049708545"That’s one higher than the nonce the account’s last UserOperation actually consumed, exactly what you’d expect from a used sequence. The takeaway: getNonce(sender, 0) == 0 is not proof an account has never transacted. Wallet SDKs are free to pick any key — some use 0 for everything, others derive a key per session or per validator module. If you need to confirm real activity rather than guess a key, pull the account’s UserOperationEvent logs from the EntryPoint and decode the nonce from there instead of assuming key 0.
Watching several paymasters or accounts at once
A gas-sponsorship dashboard usually needs more than one address checked on a schedule. Same .map() pattern as any other multi-address evmquery read — one HTTP round trip covers the whole watchlist instead of one request per address:
WATCHLIST = [ "0x6C973eBe80dCD8660841D4356bf15c32460271C9", # Circle's USDC paymaster "0xYourOwnPaymasterOrAccount",]
resp = requests.post( "https://api.evmquery.com/api/v1/query", headers={"x-api-key": os.environ["EVMQUERY_API_KEY"]}, json={ "chain": "evm_base", "schema": { "contracts": {"entrypoint": {"address": ENTRYPOINT_V07}}, "context": {"addrs": "list<sol_address>"}, }, "context": {"addrs": WATCHLIST}, "expression": "addrs.map(a, formatUnits(entrypoint.getDepositInfo(a).deposit, 18))", }, timeout=10,)deposits_eth = resp.json()["result"]["value"] # list<double>, same order as WATCHLISTValidated live against the two addresses above, this returns [0.03875192752118992, 0] — a flat list you zip back against WATCHLIST to flag whichever entry just crossed your minimum-deposit threshold. Swap getDepositInfo(a).deposit for getNonce(a, solInt("0")) in the same call if a stalled nonce (under the key you know that account uses) is what you’re watching for instead.
v0.6 vs v0.7: same job, one small ABI difference
Both versions expose identical getNonce, getDepositInfo, and balanceOf signatures, so every example above works unchanged against either address — swap ENTRYPOINT_V07 for the v0.6 address and nothing else needs to change. The one difference worth knowing: v0.6 additionally exposes SIG_VALIDATION_FAILED(), a constant-returning view function some validation code checks against; v0.7 drops it and adds supportsInterface (ERC-165) instead. Neither affects the deposit/nonce reads in this guide, but if you’re writing code that needs to detect which version an address is before calling anything version-specific, that’s the field to probe.
ERC-4337 vs EIP-7702 — don’t conflate the two
These come up in the same searches, but solve different problems. ERC-4337 accounts are ordinary contract wallets that have always been contracts — the EntryPoint reads in this guide work because account is an address with real contract code, full stop. EIP-7702 instead lets a plain EOA temporarily delegate to a contract’s code at its own address, so a wallet that was an EOA yesterday can look like a smart account today. The EntryPoint doesn’t care which case it’s looking at — getNonce and getDepositInfo work identically either way, since both just take an address. What evmquery can’t currently do is check whether a given address carries a 7702 delegation designator in the first place, since that requires an eth_getCode-equivalent raw bytecode read, which isn’t part of the CEL surface today. If you already know an address is a 4337 sender — from a factory event, a UserOperation log, or your own app’s records — everything in this guide applies regardless of whether that sender is a classic contract wallet or a freshly-delegated EOA.
Building this into an alerting pipeline instead of a one-off script? Blockchain monitoring in Python with evmquery turns any query on this page into a polling loop. evmquery for developers covers the full API surface, including the MCP server if you’d rather wire this into an agent than a script.
Next steps
- Permit2 allowances: the second approval layer: another case where the state that matters lives in a shared singleton, not the token or account itself
- Multicall3: batch EVM contract reads: the batching pattern behind combining
getNonceandgetDepositInfoin one round trip - Blockchain monitoring in Python with evmquery: turn a deposit or nonce check into a scheduled alert
- evmquery for developers: the full API surface, MCP server, and REST reference



