Skip to Content
Wallets & auth

Wallets & auth

FudFi is wallet-first. There are three ways to own an identity: an EVM wallet (primary, going forward), a Solana wallet (legacy, still supported), or Twitter OAuth. They can be linked together on one account.

Identity model

Two columns hold wallet identities on users:

ColumnHoldsNotes
wallet_addressSolana (base58)The original identity column.
evm_addressEVM (lowercase hex)unique. Added with the EVM auth flow.

Never look a user up by one column. Always resolve through userIdentityCondition(users, { eq, or }, value) from backend/utils/identity.js, which matches Solana, raw EVM, and lowercased EVM input. It is applied in users, posts, votes, articles, replies, stats, payments, postProcessor, and adminAuth.

For frontend compatibility, an EVM-logged-in user also gets user.walletAddress = <evm address>, plus user.evmAddress and AuthContext.walletType ('solana' | 'evm'). That way headers, profile, and posting work unchanged.

EVM (primary)

EVM login is restricted to Robinhood Chain only:

NetworkChain IDEnv
Robinhood Chain4663NEXT_PUBLIC_EVM_CHAIN=mainnet
Robinhood Chain Testnet46630NEXT_PUBLIC_EVM_CHAIN=testnet (default)

The client UI is Reown AppKit (@reown/appkit + wagmi + Solana adapters, chains defined in frontend/src/lib/evm.ts). It needs NEXT_PUBLIC_REOWN_PROJECT_ID; if that is missing the EVM modal disables gracefully but hooks still work because WagmiProvider is always mounted.

1. Request a challenge

POST /api/auth/evm/challenge Content-Type: application/json { "address": "0xAbC...", "chainId": 46630 }

Returns the exact message to sign, the nonce, chainId, and expiresAt. Nonces are single-use and expire after 10 minutes. An unsupported chain returns 400.

2. Sign the message

Sign the returned message verbatim with the wallet (EIP-191 personal message).

3. Verify

POST /api/auth/evm/verify Content-Type: application/json { "address": "0xAbC...", "message": "...", "signature": "0x...", "nonce": "..." }

The nonce is consumed on use. Signatures are checked with ethers@5 verifyMessage (CJS-safe — do not upgrade to ethers v6 ESM). On success the user is found by evm_address or created with username evm_<first-6-hex>.

Solana (secondary / legacy)

Connect a Solana wallet and register — no signature required (legacy behavior, do not “fix” it):

POST /api/users/get-or-create Content-Type: application/json { "walletAddress": "<base58>" }

Sending an existing address returns the existing user; a new one is created with defaults.

Twitter OAuth

GET /api/auth/twitter # returns { authUrl, state } GET /api/auth/twitter/callback # exchanges code, logs in or creates the user

OAuth state is kept in an in-memory oauthStates map with a 10-minute TTL (fine for single-instance). Pass ?link=true&userId=<id> to link Twitter to an existing account instead of logging in.

Linking & unlinking

EndpointMethodNotes
/api/auth/link-evmPOSTSignature required (message, signature, nonce).
/api/auth/link-walletPOSTLinks a Solana wallet.
/api/auth/link-wallet / Twitter OAuth—Twitter links through the OAuth flow above.
/api/auth/unlink-evmPOSTBlocked if it is the only method.
/api/auth/unlink-walletPOSTBlocked if it is the only method.
/api/auth/unlink-twitterPOSTBlocked if it is the only method.

The unlink guards count all three methods (Solana + EVM + Twitter). When editing auth, always check all three before allowing an unlink.

Session lookup

GET /api/auth/me?userId=<id> # returns { user }

Environment

# frontend NEXT_PUBLIC_REOWN_PROJECT_ID=... NEXT_PUBLIC_EVM_CHAIN=testnet # or mainnet NEXT_PUBLIC_EVM_RPC_URL=... # optional override for the testnet RPC # backend (Twitter OAuth) TWITTER_CLIENT_ID=... TWITTER_CLIENT_SECRET=... TWITTER_CALLBACK_URL=...