Troubleshooting

Wallet sign-in failures and what each one actually means — the 500 that is a missing variable, the success toast that leaves no session, and the lockout that arrives after two attempts.

8 min readUpdated 6 August 2026troubleshooting, siwe, 403, 401, 429

Nearly every report about this addon is one of the entries below. The common thread is that the failure surfaces a long way from its cause: a browser-side success followed by a server-side refusal, or a 500 that names a signature problem when the real fault is an unset environment variable.

Start by reading the backend log for the request. The wallet routes narrate themselves step by step under the LOGIN and WALLET modules, and the last step logged is almost always the answer.

Sign-in and linking

NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID is not set in the backend's environment, or the backend has not been restarted since it was.

This is the most misleading failure in the addon, because the frontend hides it. frontend/config/wallet.tsx falls back to a hard-coded project ID when the variable is absent, so the wallet picker opens, wallets connect and everything looks correct right up to the moment the signature is posted. The backend has no fallback.

Check the variable, then remember it is read in two different ways: the frontend inlines it at build time, the backend reads it once at module load.

grep NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID .env
pnpm build:frontend
pnpm restart

A frontend rebuild alone leaves the backend on the old value; a restart alone leaves the old value compiled into the bundle.

The wallet_connect extension is not enabled. Go to Admin → System → Extensions and switch it on; the enabled-extension cache is invalidated on toggle, so no restart is needed.

If the toggle is already on, the licence file is the other half of the check. The handlers require lic/37548018.lic to exist in the project root, and answer "Wallet Connect extension license is not activated" when it does not. Verify the licence from the same screen — that is what writes the file.

This 403 also explains a failure that does not look like a 403 at all. The Wallet tab in the user profile is not hidden when the extension is disabled, so a user can reach it, connect a wallet, and get a generic "Connection Error — Failed to connect wallet" toast. That toast is the nonce request being refused with 403.

There is no provider_user row for the address that signed. Either the wallet was never linked, or a different address signed than the one you expect.

The second case is common with multi-account extensions. The signature request goes to window.ethereum and is signed by whichever account that provider currently has selected — not necessarily the account the Reown modal is displaying. Ask the user to switch to the intended account inside their wallet extension, reload, and try again.

To confirm what is linked, read /api/user/profile as that user and look at the providers array, or query the table directly:

SELECT userId, providerUserId, createdAt
FROM `provider_user`
WHERE provider = 'WALLET';

Remember that linking must happen first, from a signed-in session. There is no sign-up-with-wallet path.

Verification is a call from your backend to rpc.walletconnect.org. Three causes, in order of likelihood:

  1. The backend has no outbound HTTPS. Locked-down egress produces exactly this error, with a Signature verification error line in the log. Test from the app server, not from your laptop.
  2. The project ID is wrong or has been revoked. It is sent in the RPC query string; an invalid one gets no useful answer.
  3. A smart-contract wallet on a chain where its contract is not deployed. EIP-1271 verification asks the contract on the chain named in the message. A Safe that exists on mainnet cannot sign in on Base.

The nonce lives for 300 seconds and is deleted the first time it is used.

Ordinary causes: the user left the signature prompt open for more than five minutes, or the request was retried after it had already succeeded once. Ask them to start again.

Structural causes worth checking if it happens constantly: Redis was flushed or restarted mid-flight, or the message was assembled without the server's nonce. If a wallet or in-app browser mangles the message, the Nonce: line may not survive parsing and the response is "Invalid or missing login nonce" instead — a different message that points at the same place.

Both the nonce endpoint and the sign-in endpoint are on the shared strict limiter: five requests per fifteen minutes, keyed by IP address.

One complete sign-in costs two of those five, so the real budget is two attempts, not five. Neither endpoint resolves an optional session, so a signed-in user linking a wallet is still counted by IP — and everyone behind the same NAT shares the count.

The limiter also fails closed. If Redis is erroring, every request is refused with a 429 rather than allowed through, so a 429 that arrives on the first attempt of the day is a Redis problem, not a user problem.

There is no way to raise the limit from a screen or an environment variable.

There is no injected wallet in the browser. Both the login form and the profile Wallet tab build their signer from window.ethereum rather than from the connector AppKit just connected.

The practical consequence: connecting from a desktop browser by scanning a WalletConnect QR code with a phone succeeds at the connect step and then stops here, because nothing was injected into the desktop page. Users on mobile should open the site inside their wallet's own browser, where a provider is injected. Users on desktop need an extension.

The button renders only when /api/settings lists wallet_connect among the enabled extensions. If it is absent, the extension is off or unlicensed — see the 403 entry above.

Note that the modal degrades rather than errors: if a user reaches the wallet-login view while the extension is disabled, they are shown the ordinary password form instead.

After sign-in

The account has 2FA enabled.

The backend does the correct thing: it issues a two-factor challenge, delivers the code, and returns 200 with twoFactorToken and no session cookies. The wallet login form has no branch for that response. It treats any 200 as success, tries to read /api/user/profile, gets a 401, ignores it, shows the success toast and closes the modal.

The user sees success, receives an SMS or email code they are never asked for, and remains anonymous. The password login form does render the challenge, so the workaround is to sign in with a password.

If you run wallet sign-in as a headline feature, decide deliberately: either 2FA-enabled users use passwords, or wallet-only users do not enable 2FA. There is no setting that disables the challenge for wallet sign-in alone.

The NFT flows read user.walletAddress, which is a mirror of the account's primary WALLET link maintained by the afterSave hook in backend/models/access/providerUser.ts. The message means that mirror is empty, which has three causes worth separating:

  • The user has never linked a wallet. Linking one at /user/profile?tab=wallet is the whole fix.
  • They linked one and then unlinked it, with no other WALLET link to be promoted in its place. Same fix.
  • The install predates the mirror, so links exist but no row was ever marked primary. Run backend/scripts/unify-wallet-address-stores.mjs — dry-run first, and read what it says it will quarantine.

It gates buying a listing, transferring a token, approving a contract, confirming an offer, deploying a collection and settling an auction. The full contract is in Linking a wallet.

Same field. The completion score counts ten and walletAddress is one of them, so 100% needs a linked wallet.

The dashboard card reads user.walletAddress — the primary link — while the Wallet tab reads the providers array and shows whichever address the browser is currently connected to. They disagree when the user has two linked wallets and is connected to the one that is not primary. Only the primary address is mirrored, and only that one is what an NFT settlement would pay.

By design. The wagmi storage adapter in frontend/config/wallet.tsx refuses to persist the recent-connector and store keys specifically to prevent silent auto-reconnection on page load. A returning user is connected to your platform by their session cookie, not by a live wallet connection, so the wallet only needs to be present when they are signing something.

Configuration and upgrade

Older documentation for this feature told operators to run pnpm updator:frontend after setting the project ID. No such script exists in this release. Use either the full update:

pnpm updator

or, when only the environment changed:

pnpm build:frontend
pnpm restart

Because it is a NEXT_PUBLIC_ variable, its value is compiled into the JavaScript bundle at build time. Restarting the frontend serves the same bundle. You must run pnpm build:frontend.

The backend reads the same variable once at module load, so it needs a restart. Doing one and not the other produces the worst state: a frontend on the new project ID and a backend verifying against the old one, which fails as "Signature verification failed".

The chain has to be on both lists — the picker in frontend/config/wallet.tsx and the allow-list in backend/src/api/auth/utils.ts. A chain on the picker but not the allow-list is refused with 401 and an Unsupported SIWE chainId line in the log.

Stock installs offer six networks and accept eight, so this only occurs after someone has edited one file and not the other. The table is in Wallets, chains and endpoints.

Most often the address is already linked to a different account. providerUserId is unique across the whole table, so the insert violates the constraint and the handler answers 500 rather than explaining the collision.

Confirm with:

SELECT userId, provider FROM `provider_user`
WHERE providerUserId = '0x…';

The existing owner has to disconnect the address before anyone else can claim it. Unlinking is a hard delete, so the address is genuinely released.

There is no admin screen for it. The disconnect endpoint acts only on the signed-in user's own links, and the admin user editor cannot write wallet fields. Deleting the provider_user row directly is the only option, and it is safe — the row is nothing but a credential mapping, and the user keeps their password and email login.

Reading the logs

The four handlers narrate their progress, so the last recorded step names the check that failed.

Log module Route Typical last step
LOGIN POST /api/auth/login/wallet Consuming single-use login nonce, Verifying signature for address: …, Looking up wallet provider
WALLET POST /api/user/profile/wallet/connect Consuming single-use connect nonce, Registering wallet address
WALLET POST /api/user/profile/wallet/disconnect Finding wallet connection
AUTH signature verification Unsupported SIWE chainId: …, Signature verification error

A successful sign-in also writes an auth.login activity row on the user's record with the truncated address in its description, which is the quickest way to confirm from the admin panel that a wallet sign-in actually happened.