Post 11
Wallet Connection in 2026: EIP-6963 and the Connector Layer
window.ethereum was always a race condition. EIP-6963 fixed multi-wallet discovery, WalletConnect v2 handles mobile, and the connector abstraction hides both. What is actually happening when a user clicks 'Connect', and how to get the edge cases right.

For years, connecting a wallet meant reading window.ethereum and hoping. If the user had two extension wallets installed, one would silently win the global, and 'Connect MetaMask' might open Rabby. EIP-6963 replaced that global free-for-all with a proper announcement protocol, and the connector layer in libraries like wagmi now hides the whole mess. Worth understanding what it is hiding.
EIP-6963: wallets announce themselves
Instead of fighting over window.ethereum, each injected wallet dispatches an event with its own provider, a name, an icon, and a unique id. The page asks for announcements on load and gets back a list.
1type Wallet = {2 info: { uuid: string; name: string; icon: string; rdns: string };3 provider: EIP1193Provider;4};5 6function discoverWallets(): Promise<Wallet[]> {7 return new Promise((resolve) => {8 const found: Wallet[] = [];9 window.addEventListener("eip6963:announceProvider", (e: any) => {10 found.push(e.detail);11 });12 window.dispatchEvent(new Event("eip6963:requestProvider"));13 setTimeout(() => resolve(found), 100); // give wallets a tick to answer14 });15}16// now you can render a real list — each with its own provider,17// no more guessing which one window.ethereum points atThe connector abstraction
A connector is a uniform interface over 'a way to get an EIP-1193 provider and an account'. Injected (6963), WalletConnect (QR / deep link), Coinbase Wallet SDK, a Safe app context, a passkey-based smart account — all the same shape to your code.
1import { createConfig, http } from "wagmi";2import { mainnet, base } from "wagmi/chains";3import { injected, walletConnect, coinbaseWallet } from "wagmi/connectors";4 5export const config = createConfig({6 chains: [mainnet, base],7 connectors: [8 injected(), // picks up every EIP-6963 wallet automatically9 walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! }),10 coinbaseWallet({ appName: "My App" }),11 ],12 transports: {13 [mainnet.id]: http(),14 [base.id]: http(),15 },16 ssr: true, // don't touch window during SSR17});1"use client";2import { useConnect, useAccount, useDisconnect } from "wagmi";3 4export function Connect() {5 const { connectors, connect, status, error } = useConnect();6 const { address } = useAccount();7 const { disconnect } = useDisconnect();8 9 if (address) {10 return <button onClick={() => disconnect()}>{short(address)}</button>;11 }12 13 return (14 <div>15 {connectors.map((c) => (16 <button key={c.uid} onClick={() => connect({ connector: c })}17 disabled={status === "pending"}>18 {c.icon && <img src={c.icon} alt="" width={20} />}19 {c.name}20 </button>21 ))}22 {error && <p>{friendlyConnectError(error)}</p>}23 </div>24 );25}The edge cases that matter
| Situation | What to do |
|---|---|
| No wallet installed | Show install links, not a broken 'Connect' button. Detect: connectors list is empty of injected. |
| User rejects the connection | error.name === 'UserRejectedRequestError' — that's a normal outcome, not an error toast |
| Wallet locked | The connect promise hangs until they unlock. Show 'Check your wallet' and a cancel. |
| Multiple accounts | You get the first. Listen for accountsChanged and update — wagmi does this. |
| Reconnect on reload | config.storage + wagmi's autoConnect. Don't prompt again if they were connected. |
| Running inside a Safe / Frame | Detect the context and offer that connector first, or auto-connect. |
Persist and reconnect without a prompt
1"use client";2import { WagmiProvider, cookieToInitialState } from "wagmi";3import { config } from "@/lib/wagmi";4 5export function Providers({6 children, cookie,7}: { children: React.ReactNode; cookie: string | null }) {8 // hydrate connection state from the SSR cookie — no flash of9 // 'disconnected' on first paint, no re-prompt10 const initialState = cookieToInitialState(config, cookie);11 return (12 <WagmiProvider config={config} initialState={initialState}>13 {children}14 </WagmiProvider>15 );16}Mobile: WalletConnect and deep links
On a phone, the user is in Safari and their wallet is a separate app. WalletConnect v2 handles the handoff — a QR on desktop, a deep link on mobile that opens the wallet app, they approve, and the connection is relayed back. The relay is a dependency; if projectId is missing or the relay is down, mobile connection silently fails, so surface that state.
The connector layer means you write one 'Connect' button and it works for a browser extension, a mobile wallet, a hardware wallet, a Safe, and a smart account. Understanding EIP-6963 underneath it is what lets you debug the day one of them misbehaves.
Found this useful? Pass it on.
Want a custom write-up for your team? Get in touch.
Building something like this?
If a post here maps to a problem on your roadmap, that's usually a good sign we should talk.