Why
The UI usually collapses session state to "connected." That hides which methods remain authorized and produces stale-account bugs when the wallet changes state outside the application.
A WalletConnect session contains accounts, chains, methods, events, and expiry. Treating it as a boolean isConnected discards the information that says what the application may request and whether its cached account is still current.
How it works
Connect with the smallest method set, compare it with a broad default proposal, then exercise account change, chain change, session update, expiry, relay disconnect, and explicit revocation.
PoC
Create one minimal proposal and one broad proposal using the WalletConnect Sign client or Reown AppKit. Exercise account and chain changes, a session update, expiry, relay loss, reconnect, and explicit disconnect. Assert that each event invalidates or narrows application state correctly.
What it proves
Connection UX is capability management. The safe default is the smallest requested namespace and method set, an observable session state machine, and a visible revocation path — not a green dot that survives stale state.
Reference: Reown WalletConnect session usage.
Review clarification
Same disease, different organ — from lifecycle to authorization
The title is the whole reframe: connect once = authorize a session, not exchange an address. A session is a standing capability — scope + lifetime + mutation events — and a boolean isConnected is a lossy projection that throws almost all of it away. It is the authorization-side member of the pattern named in a-boolean-hides-a-state-machine.
The green dot conflates three independent layers
This is the exact structural error from private-rpc-visibility and bundler-paymaster-dependencies, now in the wallet:
| Layer | The real question | What the green dot pretends |
|---|---|---|
| Relay / transport | can I currently talk to the wallet? | "connected" |
| Session / authorization | is the grant still valid (not expired/revoked)? | "connected" |
| Account / state | is my cached account/chain still current? | "connected" |
A relay disconnect does not invalidate the session; a reconnect does not re-authorize. Transport availability is not authorization — the same line bundler-paymaster-dependencies draws for "accepted ≠ settled."
The security core is least privilege
A session names its namespaces, chains, methods, and accounts (EIP-155 / CAIP-2 / CAIP-10). Requesting a broad default — every chain, every method, eth_sign included — is the OAuth "this app wants access to everything" antipattern landing in web3. The method set is the attack surface: a session authorized for eth_sign (blind signing) is a categorically bigger liability than one scoped to personal_sign. So the safe default is the smallest requested namespace and method set, and the minimal-vs-broad proposal comparison is what makes that difference measurable rather than assumed. Same "what did you actually authorize" question as embedded-wallet-policy and agentic-intent-veto, one layer down at the transport standard.
Stale-account is the concrete, dangerous bug
The wallet can change state outside your app — the user switches account, switches chain, lets the session expire, or revokes. If the UI holds a cached account behind a green dot, it will display balances for, or request a signature from, the wrong account or the wrong chain. The accountsChanged / chainChanged / session_update / session_delete / session_expire events exist precisely to narrow or invalidate app state; ignoring them is the green dot that survives stale state. That is rpc-view-not-consensus's lesson — one party's cached view is not ground truth — applied to your own session cache.
The invariant: every event narrows or invalidates, never silently widens
That is what "assert each event narrows or invalidates application state correctly" really encodes. Authority should only shrink or require a fresh grant; it must never quietly grow. A session that appears to gain a method or an account without an explicit re-proposal is a bug — or an attack. Modeling the session as an explicit state machine (proposed → established → updated → expired/disconnected/revoked → re-proposed) is what makes the invariant checkable.
Expiry and revocation are first-class, not edge cases
A session past expiry is not authority — treating it as connected is the authorization-domain twin of receipt-is-not-settlement (a thing that looks done but is not valid). And revocation must be visible and bidirectional: the user can session_delete from the wallet, and the app must surface its own revocation path. A "connected" you cannot cleanly observe or revoke is a lingering capability — the forgotten-OAuth-grant failure mode, which is the who-holds-the-mint / "who can halt" theme in the authorization domain.