Mirror / fallback: https://gist.github.com/riyannode/fe20691e0a0bdfbe9144b24538113757
Arc Layer Privy AutoSwitchArcChain — Ground Truth
Prompt
I'm building a Next.js 14 App Router dApp for Arc Testnet (chainId 5042002) using @privy-io/react-auth + @privy-io/wagmi + wagmi v2. Users log in via Privy with EXTERNAL wallets (MetaMask, Rabby, OKX) — NOT embedded wallets.
Problem: after the user picks MetaMask in the Privy model, Privy handshakes the wallet and then shows a 'Sign to verify' SIWE prompt. MetaMask signs on whatever chain it was already on (usually Ethereum mainnet 0x1), not Arc Testnet. Setting defaultChain: arcTestnet and supportedChains: [arcTestnet] in PrivyProvider config DOES NOT help — those only affect embedded wallets.
Write a headless React component AutoSwitchArcChain.tsx that:
- Returns null (no UI)
- Mounts inside
<WagmiProvider> - Auto-calls
switchChain(5042002)the MOMENT an external EVM wallet handshakes, BEFORE the Privy SIWE 'Sign to verify' modal appears, so the user signs on Arc Testnet - Does NOT fire repeatedly on the same wallet (avoid infinite re-render loops)
- Gracefully ignores non-Ethereum wallet types (e.g. Solana if included in loginMethods)
Constraints:
- Must correctly handle the pre-SIWE window.
authenticatedis still FALSE when the wallet handshakes — the sign happens BEFOREauthenticatedflips true - Must use Privy's own
useWallets()hook, not wagmi'suseSwitchChain - Must use a ref (not state) to track in-flight switches
Model
claude-opus-4.7 (also tested: sonnet-4.7, gpt-5.5, gemini-2.5, grok-4 — same wrong pattern)
Ground Truth
Explanation
Three interlocking traps:
1. Auth-flow timing. Every model defaults to useEffect(() => { if (authenticated) wallet.switchChain(...) }, [authenticated]) because that matches the normal wagmi + wallet-auth mental model. But authenticated flips true at t3 (AFTER SIWE sign), not t1. The SIWE sign itself at t2 is what we need to happen on Arc. Correct gate: wallets.length > 0 fires at t1.
2. Hook choice. wagmi's useSwitchChain() is the canonical way, but @privy-io/wagmi's connector registers external wallets to wagmi asynchronously. At t1 (pre-SIWE), wagmi hooks often haven't seen the wallet yet, while Privy's useWallets() sees it immediately on handshake. Models reach for useSwitchChain() first because it's wagmi canon.
3. State vs Ref. useState<Set<string>> to track in-flight switches triggers a re-render on every add/delete, which re-runs the effect, which re-fires switchChain on the same wallet while its popup is still open -> MetaMask infinite loop. Ref is the only way that doesn't deadlock. Models default to useState because 'state changes should drive re-renders' is a React rule-of-thumb.
Derivation
Debugged live on ArcLayer production console (Privy app cmopoaivr00kw0cl7pl9m3noz, Arc testnet chain 5042002) with console.log on every Privy state transition during real Rabby + MetaMask + OKX logins. Observed authenticated flipped ~800ms AFTER the MetaMask signature. Confirmed defaultChain is embedded-wallet-only via Privy GitHub issues. Ran the fixed version in prod for 2 weeks with ~50 logins, zero SIWE-on-wrong-chain incidents.
Why this isn't trivially solvable
- NOT in Privy docs (docs still use
authenticated-gated effects) - NOT a model-age issue (tested across 6 frontier models, same wrong pattern)
- NOT a missing-context issue (giving Privy + Arc docs doesn't help — docs themselves are wrong/incomplete here)
- Failure mode is SILENT — SIWE completes successfully, user just ends up signed-in on Ethereum mainnet instead of Arc, manifests as 'tx reverts' much later, almost impossible to trace without instrumenting the auth flow