Sign-in with Ethereum

Sign in with a wallet, using EIP-4361, beside email codes.

Users can sign in with an Ethereum wallet as well as with an emailed code. It uses the Better Auth siwe plugin and follows EIP-4361. It arrived in #100.

How sign-in with Ethereum worksThe server gives the console a one-time nonce. The user's wallet signs a message that includes it. The server checks the domain, chain, expiry and signer, and signs the user in. Sending the same signature again is refused because the nonce is used up.CONSOLEWALLETQMESHAUTHSESSIONNONCESIGN THISSIGNATURESIGNATURESIGNED INSAME SIGNATURE AGAIN01 NONCEThe server hands out a one-time nonce02 SIGNYour wallet signs a message that includes it03 VERIFYDomain, chain, expiry and signer are checked04 REPLAYThe nonce is used up, so a replay fails

How a sign-in works

  1. The console asks the server for a nonce. A nonce is single use and expires.
  2. The wallet signs an EIP-4361 message that includes the nonce (an EIP-191 personal_sign).
  3. The server recovers the signer's address from the signature and checks the message: its domain, a chain id above zero, and its expirationTime and notBefore.
  4. The nonce is consumed atomically. Replaying a signature fails at once.

Signature checking uses @noble/curves (secp256k1) and @noble/hashes (keccak-256), without a heavy wallet library on the server. A successful reverse ENS lookup fills in the user's name and avatar.

Linked wallets

A signed-in user can attach more than one wallet. Settings has a panel to link, unlink and copy addresses, and to see ENS names.

Method and pathPurpose
POST /api/auth/siwe/linkLink a wallet to the current account after verifying the signature and nonce. Answers 409 if the address belongs to another account.
POST /api/auth/siwe/unlinkUnlink a wallet. A user cannot unlink their only way in (no email and one wallet). If the primary wallet is unlinked, the oldest remaining one becomes primary.
GET /api/auth/siwe/walletsList linked wallets with chain id, primary flag, creation date and ENS name.

Configure

NEXT_PUBLIC_APP_DOMAIN sets the expected domain when it differs from the request host. Email codes use RESEND_API_KEY and OTP_EMAIL_FROM, and sessions are stored in MongoDB (MONGODB_URI). See frontend/.env.example for the full list.

The feature ships with 16 tests covering nonce handling, message parsing, signature recovery, replay, domain, chain and expiry rejection, and the linking rules.

On this page