EIP-6963: discovering wallets without the window.ethereum race
window.ethereum was a single global that every wallet fought to own. EIP-6963 replaces the race with an event handshake - discover every installed wallet, then reach for mipd or wagmi.
Reaching for window.ethereum was the old way to connect a wallet - a single global that every browser extension fought to own. EIP-6963 is the modern replacement, yet I still see plenty of dApps wiring up that global today. So this article walks through the whole thing - the problem, the libraries you should actually use - with interactive examples you can poke at.
The window.ethereum race
Every wallet extension used to inject its EIP-1193 provider into the same window.ethereum slot. Extensions load in an unpredictable order, so it's a race condition - whoever writes last wins. Install MetaMask, Ambire, and Coinbase Wallet together and your dApp talks to whichever one happened to load last, with no clean way to let the user pick.
Three wallets, one slot
Three wallets racing to claim one global slot…
Wallets tried to patch around this with proxies and providers arrays, but there was never a standard. The result was simpler and more annoying: your dApp just connected to the wrong wallet - and the one the user actually wanted could be unreachable, even though it was installed.
The handshake: request ⇄ announce
EIP-6963 swaps the single global for two window events. On load, the dApp dispatches eip6963:requestProvider. Every wallet that supports the spec replies by dispatching eip6963:announceProvider, carrying its own provider plus metadata. The dApp collects every reply - no overwriting, no race.
The discovery flow
Each wallet replies with an announce event. No race - you keep all of them.
Wallets also announce proactively on page load, which is why the order matters: you add your listener before you dispatch the request, so an eager wallet that announces immediately isn't missed.
The raw implementation
The whole mechanism is about twenty lines of vanilla TypeScript. Listen for announcements, dedupe by rdns (the reverse-DNS wallet identifier, e.g. io.metamask), then request:
const found = new Map<string, EIP6963ProviderDetail>()
window.addEventListener("eip6963:announceProvider", (event) => {
const { info, provider } = (event as CustomEvent<EIP6963ProviderDetail>).detail;
// dedupe - a wallet may announce more than once
found.set(info.rdns, { info, provider });
renderWalletList([...found.values()]);
});
// listen first, then ask
window.dispatchEvent(new Event("eip6963:requestProvider"));When the user clicks one, you call eth_requestAccounts on that specific provider - not on a guessed global:
async function connect(detail: EIP6963ProviderDetail) {
const accounts = await detail.provider.request({
method: "eth_requestAccounts"
});
return accounts[0];
}The announced shape
Every announcement carries an info object with a stable uuid, a display name, a base64 icon data URI you can render directly, and the rdns you dedupe on:
interface EIP6963ProviderInfo {
uuid: string; // unique per page-load session
name: string; // "MetaMask"
icon: string; // data:image/svg+xml;base64,…
rdns: string; // "io.metamask" - dedupe key
}
interface EIP6963ProviderDetail {
info: EIP6963ProviderInfo;
provider: EIP1193Provider; // the real provider
}What your browser actually has
Here's the code from §3 running live against your browser right now. If you have any EIP-6963 wallets installed, they're listed below; if not, you'll see the handshake as a simulation.
Live discovery
Why not hand-roll it in production
The twenty lines are great for understanding the mechanism, but a real app hits edge cases you don't want to re-solve: wallets that announce late or re-announce on reconnect, deduping across both the request/announce cycle and proactive announcements, SSR safety (window doesn't exist on the server), and keeping the event details fully typed. That's exactly what the libraries below absorb.
The libraries to reach for
You almost never want the raw events in production. Pick by how much UX you want to own:
- mipd (by wevm) - a tiny, zero-dependency, framework-agnostic store and type set. The right pick when you want 6963 discovery and nothing else.
- wagmi v2 - has
multiInjectedProviderDiscoveryturned on by default; theinjected()connector consumes announcements automatically. The default for React dApps. - ConnectKit (by Family) - a connect-modal that shows every 6963 wallet plus WalletConnect by default. It doesn't touch the events itself: it's built on wagmi v2, so discovery comes for free (wagmi is the one pulling in
mipd). The pick when you want that discovery and a genuinely well-designed modal without building it yourself. - RainbowKit / Reown AppKit - the other full connect-modals in the same tier, also merging 6963 wallets with WalletConnect QR. Reach for any of the three when you want the whole connect UX handed to you; pick on design taste and which ecosystem you're already in.
- web3.js - exposes
requestEIP6963Providers()andonNewProviderDiscovered()if you're not on viem.
The wagmi payoff
If you're on wagmi, you've already got it. Discovery is on by default - you don't list every wallet as a connector, you just render whatever was discovered:
const config = createConfig({
chains: [mainnet],
transports: { [mainnet.id]: http() },
// multiInjectedProviderDiscovery: true is the default
});
function WalletList() {
const { connectors, connect } = useConnect();
return connectors.map((c) => (
<button key={c.id} onClick={() => connect({ connector: c })}>
{c.name}
</button>
));
}Don't delete window.ethereum yet
EIP-6963 is the preferred discovery mechanism, but window.ethereum isn't dead. Mobile in-app browsers and WebView bridges still rely on it, and older wallets may only inject the global. The good news: every library above falls back to window.ethereum when no announcements arrive, so you get the clean multi-wallet path where it's supported and graceful degradation where it isn't. Use 6963 first, keep the global as a fallback - and let a library handle the seam.
Newsletter
Stay updated with my latest articles and projects. No spam, no nonsense.
