WalletLink is a free, open-source, SaaS-independent way to connect a frontend to EVM wallets. Discovery and connection happen entirely in the browser over two finalized Ethereum standards, EIP-1193 (provider interface) and EIP-6963 (multi-injected-provider discovery): no hosted relay, no API key, no third-party service in the connection path.
It is a thin, wagmi-native drop-in replacement for the WalletConnect-based connect
stacks (RainbowKit, ConnectKit, Web3Modal).
Consumers keep every wagmi hook they already use (useAccount, useWriteContract,
…); WalletLink only replaces the connection layer.
Note
Pre-release. The package is not yet published to a registry and the API may
still change. It ships the config builder, the headless useWalletLink hook, and
the styled connect UI (WalletLinkButton, WalletLinkModal).
Reown AppKit (the connect stack formerly published as WalletConnect) is
proprietary-licensed: its Community License requires every app to connect through
Reown's hosted relay network using a projectId. That fails the free,
open-source, SaaS-independent, censorship-resistant bar Stability Nexus dapps aim
for.
Major wallets such as MetaMask and Coinbase Wallet support EIP-6963, which lets a
page discover injected wallets through a local window event handshake, with no
server in the loop. WalletLink builds on that (via wagmi, which already implements
it) so extension-wallet connection works with zero SaaS dependencies.
Cross-device connection (desktop dapp ↔ phone wallet) is the one thing that genuinely needs a relay; there is no production self-hostable WalletConnect relay, so WalletLink is injected-only for now and leaves a seam for a relay transport later.
-
createWalletLinkConfig: wagmiConfigbuilder, noprojectId. -
useWalletLink: headless connect / account hook. -
WalletLinkButton+WalletLinkModal: styled connect UI. - Published to a package registry.
- Integrated into a Stability Nexus dapp (Fate-EVM-Frontend is the proof case).
- Cross-device (mobile) support via a self-hostable relay transport.
- TypeScript
- React (peer dependency)
- wagmi v2 + viem (peer dependencies)
- @tanstack/react-query (peer dependency)
- Built with tsup (ESM + CJS + type declarations)
- Standards: EIP-1193, EIP-6963
No projectId, no relay, no hosted service.
- Node.js 18+
- An EVM wallet browser extension (MetaMask, Rabby, Frame, …) for testing
- A React app already using wagmi v2 (or willing to add it)
Not published yet. The registry command below is how you will install WalletLink once the first release is out. Until then, use it from source: clone this repo, run
npm install && npm run build, and link the result into your app (for example withnpm link, or a file/git dependency that builds thedistoutput).
Once published, install WalletLink alongside its peer dependencies:
npm install @stability-nexus/walletlink wagmi viem @tanstack/react-query
# or: yarn add / pnpm addcreateWalletLinkConfig returns a standard wagmi Config. No projectId.
// wagmiConfig.ts
import { sepolia } from 'wagmi/chains'
import { createWalletLinkConfig } from '@stability-nexus/walletlink'
export const config = createWalletLinkConfig({
chains: [sepolia],
ssr: true, // set for Next.js; enables cookie-based hydration
})Passing transports is optional; omit it and each chain gets a default http()
transport. When supplied, it is keyed to chains, so leaving a chain out is a
compile-time error rather than a runtime one.
Mount wagmi's WagmiProvider and a react-query QueryClientProvider, exactly as a
wagmi app already does:
'use client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { config } from './wagmiConfig'
const queryClient = new QueryClient()
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</WagmiProvider>
)
}useWalletLink exposes the discovered wallets, connect/disconnect, and the current
account. Bring your own UI:
'use client'
import { useWalletLink } from '@stability-nexus/walletlink'
export function Connect() {
const { wallets, connect, disconnect, address, isConnected, pendingWallet } =
useWalletLink()
if (isConnected) {
return <button onClick={() => disconnect()}>{address}</button>
}
// Empty during SSR and the first client render (see the note below), so show a
// discovery state rather than a bare, wallet-less control.
if (wallets.length === 0) {
return <p>Looking for wallets…</p>
}
return wallets.map((wallet) => (
<button
key={wallet.uid}
onClick={() => connect(wallet)}
disabled={pendingWallet?.uid === wallet.uid}
>
{wallet.name}
</button>
))
}
walletsis empty during SSR and on the first client render, because EIP-6963 discovery is a browser-only handshake, so wagmi appends the announced wallets onceWagmiProvidermounts. Render a loading/empty state rather than concluding no wallet is installed.
If you would rather not build the UI, render WalletLinkButton. It packages the
whole flow: a connect button that opens a wallet-picker modal, and, once connected,
an account avatar with a menu to copy the address or disconnect.
'use client'
import { WalletLinkButton } from '@stability-nexus/walletlink'
export function Header() {
return <WalletLinkButton />
}label sets the disconnected button text (default Connect Wallet). The modal
lists the EIP-6963 wallets the browser announced; for popular wallets that are not
installed it shows an install link instead (WalletLink has no relay, so an absent
wallet can be installed but not connected). It needs no CSS import: the components
inject their own stylesheet on first mount.
For a custom trigger, drive WalletLinkModal yourself with your own open state:
'use client'
import { useState } from 'react'
import { WalletLinkModal } from '@stability-nexus/walletlink'
export function Connect() {
const [open, setOpen] = useState(false)
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Connect
</button>
<WalletLinkModal open={open} onOpenChange={setOpen} />
</>
)
}The connect UI ships with a neutral light/dark theme and needs no configuration. It is built on unstyled Radix primitives skinned by a single stylesheet the components inject on first mount, so there is nothing to import and no Tailwind config to add on the consumer side.
The default theme is light. WalletLink switches to dark when your app marks the page dark the way most apps already do, either signal works:
- a
darkclass on<html>(Tailwind's class strategy,class="dark"), or <html data-theme="dark">.
It deliberately does not follow the OS prefers-color-scheme on its own, so an
app that stays light while the OS is dark is not forced dark against its will.
Every color, radius, and font is a CSS custom property read from :root. Set any of
them on :root (or on any element that contains the button) and WalletLink picks it
up, with no build step and no !important: the library's own rules are written at
zero specificity, so a plain selector always wins.
:root {
--walletlink-accent: #7c3aed; /* connect button + focus/spinner accent */
--walletlink-radius: 16px; /* modal corners */
--walletlink-radius-sm: 10px; /* button + row corners */
--walletlink-font: 'Inter', sans-serif;
}The full set (each has a sensible light and dark default):
| Variable | What it controls |
|---|---|
--walletlink-accent / -accent-hover |
Primary button, focus ring, spinner |
--walletlink-accent-fg |
Text on the accent button |
--walletlink-danger |
Disconnect item, error text |
--walletlink-surface |
Modal and menu background |
--walletlink-fg / -muted |
Primary and secondary text |
--walletlink-border / -hover |
Dividers and hover backgrounds |
--walletlink-overlay / -shadow |
Modal backdrop and elevation |
--walletlink-radius / -radius-sm |
Corner rounding |
--walletlink-font |
Font family (inherits by default) |
--walletlink-avatar-bg / -line / -fg |
Connected-state avatar fill/outline |
To theme dark mode independently, set the variables under your dark selector:
:root.dark {
--walletlink-accent: #a78bfa;
}We welcome contributions of all kinds! To contribute:
- Create a feature branch (
git checkout -b feat/your-feature). - Make your changes and keep them focused: one purpose per pull request.
- Run the quality checks before committing:
npm run format:writenpm run lint:fixnpm run typechecknpm run build
- Sign off your commits (
git commit -s); this project uses the Developer Certificate of Origin; add yourself to Contributors.md. - Open a pull request against
mainfor review.
If you encounter bugs, need help, or have feature requests, please open an issue with clear detail and any relevant logs.
WalletLink is licensed under the GNU General Public License v3.0 (or later) with
a linking exception, SPDX GPL-3.0-or-later WITH Classpath-exception-2.0. See
LICENSE.md.
The linking exception means an application can link WalletLink from its own, independent code (code not derived from WalletLink) and keep that code under the license of its choice; linking alone does not make the application GPL, as long as each linked module's own license terms are met. The copyleft still applies to WalletLink itself: modifications to this library stay free software under the GPL.
© 2025 The Stable Order.