Skip to main content

Login Flow & Account Creation

This guide explains how users authenticate with Spritz, how accounts are created, and when smart wallets are provisioned.

Overview

When a user authenticates with Spritz, they receive two things:

  1. Spritz ID — A unique identifier for social features (messaging, profiles, friends)
  2. Spritz Wallet — A Safe Smart Account for on-chain transactions

The source of these depends on the authentication method used.

┌─────────────────────────────────────────────────────────────────────────┐
│ Spritz Account Architecture │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ SPRITZ ID │ │
│ │ (Social Identity - Messages, Profiles) │ │
│ │ │ │
│ │ Source depends on auth method: │ │
│ │ • Wallet → Wallet address (0x...) │ │
│ │ • Passkey → Derived from credential ID │ │
│ │ • Email → Derived from email hash │ │
│ │ • World ID → nullifier_hash │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ SPRITZ WALLET │ │
│ │ (Safe Smart Account - Tokens, NFTs) │ │
│ │ │ │
│ │ • Computed deterministically (counterfactual) │ │
│ │ • Same address on all EVM chains │ │
│ │ • Deployed on first transaction │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

Authentication Methods Summary

MethodSpritz ID SourceWallet SignerImmediate Wallet?Best For
EVM WalletWallet addressWallet EOA✅ YesCrypto-native users
PasskeyCredential ID hashWebAuthn P-256✅ YesNew users, mobile
EmailDerived hashPasskey (must create)❌ NoNon-crypto users
World IDnullifier_hashPasskey (must create)❌ NoPrivacy-focused
Alien IDalienAddressPasskey (must create)❌ NoAlien ecosystem
SolanaSolana addressPasskey (must create)❌ NoSolana users

Flow 1: Wallet Login (SIWE)

The most common flow for crypto-native users connecting with MetaMask, Rainbow, etc.

Step-by-Step Flow

┌─────────────────────────────────────────────────────────────────────────┐
│ WALLET LOGIN FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ STEP 1: User clicks "Connect Wallet" │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Reown AppKit opens → User selects wallet → Wallet connected │ │
│ │ Result: walletAddress = 0x1234... │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 2: Get SIWE message and nonce from server │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ GET /api/auth/verify?address=0x1234... │ │
│ │ Response: { message: "...", nonce: "abc123..." } │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 3: Sign the SIWE message │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ "app.spritz.chat wants you to sign in with your Ethereum │ │
│ │ account: 0x1234... │ │
│ │ Nonce: abc123... │ │
│ │ Issued At: 2026-01-22T10:00:00Z" │ │
│ │ │ │
│ │ → Wallet prompts user to sign │ │
│ │ → signature = 0x9876... │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 4: Verify signature and create session │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ POST /api/auth/verify │ │
│ │ Body: { address, message, signature } │ │
│ │ │ │
│ │ Server: │ │
│ │ ✓ Verifies SIWE signature │ │
│ │ ✓ Creates/updates user record in database │ │
│ │ ✓ Sets wallet_type = "wallet" │ │
│ │ ✓ Creates JWT session (7-day expiry) │ │
│ │ ✓ Sets HTTP-only session cookie │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 5: Smart Wallet address computed (NOT deployed yet) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ GET /api/wallet/smart-wallet │ │
│ │ │ │
│ │ Server calculates: │ │
│ │ smartWalletAddress = getSafeAddress(walletAddress, chainId) │ │
│ │ │ │
│ │ Response: { │ │
│ │ spritzId: "0x1234...", │ │
│ │ smartWalletAddress: "0xABCD...", // Counterfactual │ │
│ │ isDeployed: false, // Not deployed yet │ │
│ │ walletType: "wallet", │ │
│ │ canSign: true, │ │
│ │ signerType: "eoa" │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ✅ LOGIN COMPLETE │
│ • Spritz ID = Wallet address (0x1234...) │
│ • Smart Wallet = Counterfactual Safe address (0xABCD...) │
│ • Wallet is NOT deployed on-chain yet │
│ │
└─────────────────────────────────────────────────────────────────────────┘

When is the Smart Wallet Deployed?

The Safe smart wallet is deployed on the first transaction. This is called "counterfactual deployment":

┌─────────────────────────────────────────────────────────────────────────┐
│ SMART WALLET DEPLOYMENT │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ BEFORE FIRST TRANSACTION: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Smart Wallet Address: 0xABCD... │ │
│ │ On-chain status: NO CONTRACT CODE (empty address) │ │
│ │ Can receive tokens: ✅ YES (address exists, just no code) │ │
│ │ Can send tokens: ❌ NO (no contract to execute) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ USER INITIATES FIRST TRANSACTION: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ e.g., "Send 10 USDC to 0x5678..." │ │
│ │ │ │
│ │ The ERC-4337 bundler automatically: │ │
│ │ 1. Deploys the Safe contract (via initCode) │ │
│ │ 2. Executes the requested transaction │ │
│ │ │ │
│ │ Gas is sponsored by Pimlico (on L2s) or paid in USDC (mainnet) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ AFTER FIRST TRANSACTION: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Smart Wallet Address: 0xABCD... │ │
│ │ On-chain status: SAFE CONTRACT DEPLOYED ✅ │ │
│ │ Can receive tokens: ✅ YES │ │
│ │ Can send tokens: ✅ YES │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Why Counterfactual Deployment?

This approach saves gas costs for users who never make on-chain transactions. The address is deterministic, so users can receive tokens immediately, and the contract is only deployed when actually needed.


Flow 2: Passkey Login

For users who want passwordless login using Face ID, Touch ID, or Windows Hello.

Step-by-Step Flow

┌─────────────────────────────────────────────────────────────────────────┐
│ PASSKEY LOGIN FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ FIRST TIME (Registration): │
│ ───────────────────────── │
│ │
│ STEP 1: User clicks "Create Passkey" │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ GET /api/passkey/register/options │ │
│ │ Response: { challenge, rpId: "spritz.chat", user: {...} } │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 2: Browser creates passkey credential │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ navigator.credentials.create({ │ │
│ │ publicKey: { │ │
│ │ challenge, │ │
│ │ rp: { name: "Spritz", id: "spritz.chat" }, │ │
│ │ pubKeyCredParams: [{ alg: -7, type: "public-key" }], // P-256│ │
│ │ authenticatorSelection: { userVerification: "preferred" } │ │
│ │ } │ │
│ │ }) │ │
│ │ │ │
│ │ → Face ID / Touch ID / PIN prompt │ │
│ │ → Returns: credentialId + P-256 public key (x, y coordinates) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 3: Store credential and create session │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ POST /api/passkey/register/verify │ │
│ │ Body: { credentialId, publicKeyX, publicKeyY, attestation } │ │
│ │ │ │
│ │ Server: │ │
│ │ ✓ Stores passkey in shout_passkey_credentials table │ │
│ │ ✓ Derives Spritz ID from credential (hash of public key) │ │
│ │ ✓ Creates user record with wallet_type = "passkey" │ │
│ │ ✓ Computes Safe address using getPasskeySafeAddress() │ │
│ │ ✓ Creates JWT session │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 4: Smart Wallet computed with WebAuthn signer │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ // Different from wallet login! │ │
│ │ smartWalletAddress = getPasskeySafeAddress(publicKeyX, publicKeyY)│ │
│ │ │ │
│ │ This Safe uses the P-256 passkey as its owner/signer │ │
│ │ (NOT an EOA address) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ───────────────────────────────────────────────────────────────────── │
│ │
│ RETURNING USER (Authentication): │
│ ──────────────────────────────── │
│ │
│ STEP 1: User clicks "Sign in with Passkey" │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ GET /api/passkey/login/options │ │
│ │ Response: { challenge, allowCredentials: [...] } │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 2: Browser authenticates with passkey │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ navigator.credentials.get({ │ │
│ │ publicKey: { challenge, rpId: "spritz.chat", ... } │ │
│ │ }) │ │
│ │ │ │
│ │ → Face ID / Touch ID prompt │ │
│ │ → Returns: assertion with signature │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 3: Verify and create session │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ POST /api/passkey/login/verify │ │
│ │ Body: { credentialId, authenticatorData, clientDataJSON, sig } │ │
│ │ │ │
│ │ Server verifies P-256 signature → Creates session │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ✅ LOGIN COMPLETE │
│ • Spritz ID = Derived from passkey credential │
│ • Smart Wallet = Safe with WebAuthn (P-256) signer │
│ • Can sign transactions using Face ID / Touch ID │
│ │
└─────────────────────────────────────────────────────────────────────────┘

Passkey vs Wallet: Key Differences

AspectWallet LoginPasskey Login
Spritz IDWallet address (0x...)Derived from credential hash
Safe SignerEOA (secp256k1)WebAuthn (P-256)
Safe Address CalculationgetSafeAddress(walletAddress)getPasskeySafeAddress(pubKeyX, pubKeyY)
Transaction SigningWallet promptFace ID / Touch ID
Cross-deviceNeed wallet on deviceCan use phone to sign on laptop

Flow 3: Email Login

For users without crypto wallets who want to sign in with just their email.

Step-by-Step Flow

┌─────────────────────────────────────────────────────────────────────────┐
│ EMAIL LOGIN FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ STEP 1: User enters email address │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ POST /api/email/send-code │ │
│ │ Body: { email: "user@example.com" } │ │
│ │ │ │
│ │ Server sends 6-digit verification code via Resend │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 2: User enters verification code │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ POST /api/email/verify-code │ │
│ │ Body: { email: "user@example.com", code: "123456" } │ │
│ │ │ │
│ │ Server: │ │
│ │ ✓ Verifies code is correct and not expired │ │
│ │ ✓ Creates/retrieves user with wallet_type = "email" │ │
│ │ ✓ Creates JWT session │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 3: User can chat and use social features │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ✅ Spritz ID assigned (derived from email hash) │ │
│ │ ✅ Can message friends, join channels, create agents │ │
│ │ ❌ CANNOT make on-chain transactions yet │ │
│ │ ❌ Smart Wallet address NOT computed yet │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 4: User creates passkey to enable wallet features │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ When user tries to: │ │
│ │ • View wallet balance │ │
│ │ • Send/receive tokens │ │
│ │ • Deploy a vault │ │
│ │ │ │
│ │ → Prompted to create a passkey │ │
│ │ → Same passkey registration flow as above │ │
│ │ → Smart Wallet address computed using passkey public key │ │
│ │ → wallet_type updated to "passkey" │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ✅ FULL ACCESS ENABLED │
│ • Spritz ID = Same (email-derived) │
│ • Smart Wallet = Now computed from passkey │
│ • Can sign transactions with Face ID / Touch ID │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Email Users Need Passkeys

Email-only users cannot make on-chain transactions until they create a passkey. The passkey provides the cryptographic signer needed for the Safe smart wallet.


Flow 4: World ID Login

Privacy-preserving authentication using zero-knowledge proofs.

┌─────────────────────────────────────────────────────────────────────────┐
│ WORLD ID FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ STEP 1: User verifies with World ID orb │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ • User scans iris with World ID orb │ │
│ │ • Generates ZK proof of unique personhood │ │
│ │ • Returns: nullifier_hash (unique per user per app) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 2: Verify proof and create session │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ POST /api/auth/world-id │ │
│ │ │ │
│ │ Server: │ │
│ │ ✓ Verifies ZK proof with World ID servers │ │
│ │ ✓ Creates user with wallet_type = "world_id" │ │
│ │ ✓ Spritz ID = nullifier_hash │ │
│ │ ✓ Creates JWT session │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 3: Passkey required for wallet │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ❌ Cannot make on-chain transactions until passkey created │ │
│ │ → User prompted to create passkey for wallet features │ │
│ │ → Smart Wallet computed from passkey │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘

Flow 5: Alien ID Login (SSO & Mini App)

Alien ID authentication supports two entry points: browser-based SSO and embedded Mini App inside the Alien app.

┌─────────────────────────────────────────────────────────────────────────┐
│ ALIEN ID LOGIN FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────┐ ┌────────────────────────────┐ │
│ │ SSO FLOW (Browser) │ │ MINI APP FLOW (Embedded) │ │
│ └─────────────┬──────────────┘ └─────────────┬──────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ STEP 1a: User clicks STEP 1b: App detects Alien │
│ "Sign in with Alien ID" environment via bridge │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ SignInButton opens SSO │ │ isBridgeAvailable() │ │
│ │ modal → user auths → │ │ = true → get authToken │ │
│ │ SDK returns JWT token + │ │ from getLaunchParams() │ │
│ │ tokenInfo with 'sub' │ │ or window.__ALIEN_ │ │
│ │ │ │ AUTH_TOKEN__ │ │
│ └─────────────┬────────────┘ └─────────────┬────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ STEP 2a: Client sends STEP 2b: Client sends │
│ { alienAddress, token } { token, isMiniApp: true } │
│ │ │ │
│ └────────────┬────────────────────┘ │
│ ▼ │
│ STEP 3: POST /api/auth/alien-id │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Server: │ │
│ │ ✓ Validates JWT format and expiration │ │
│ │ ✓ Extracts user identifier (sub claim) │ │
│ │ ✓ SSO: Verifies alienAddress matches token │ │
│ │ ✓ Mini App: Uses address from token directly │ │
│ │ ✓ Creates/updates user with wallet_type = "alien_id" │ │
│ │ ✓ Auto-joins user to official "alien" channel │ │
│ │ ✓ Creates JWT session cookie (7-day expiry) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ STEP 4: Passkey required for wallet (same as email/World ID) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ ✅ Social features work immediately (messaging, channels) │ │
│ │ ✅ User auto-joined to Alien channel │ │
│ │ ❌ Cannot make on-chain transactions until passkey created │ │
│ │ → User prompted to create passkey for wallet features │ │
│ │ → Smart Wallet computed from passkey │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ✅ LOGIN COMPLETE │
│ • Spritz ID = alienAddress (from JWT sub claim) │
│ • Social features = Available immediately │
│ • Smart Wallet = Requires passkey creation │
│ • Auto-joined to Alien channel │
│ │
└─────────────────────────────────────────────────────────────────────────┘

Alien SSO vs Mini App: Key Differences

AspectSSO FlowMini App Flow
Entry Point"Sign in with Alien ID" buttonAuto-detected via bridge
User InteractionUser clicks button, authenticates in modalFully automatic (zero-click)
Token SourceuseAuth() hook after modal closegetLaunchParams().authToken
Address VerificationServer verifies token matches provided addressServer extracts address from token
UI ModeFull Spritz UI with all auth optionsAlien-only mode (hides other auth)
API Call{ alienAddress, token }{ token, isMiniApp: true }

Alien ID API Endpoints

EndpointMethodPurpose
/api/auth/alien-idPOSTVerify Alien token, create session
/api/auth/sessionPOSTRefresh server session from stored token

For the complete integration guide with code examples, SDK setup, and troubleshooting, see Alien Integration.


Smart Wallet Creation Timeline

Here's a summary of when each component is created:

StageWhat HappensSpritz IDSmart Wallet AddressSmart Wallet Deployed
Before Login❌ None❌ None❌ No
After Wallet/Passkey LoginSession created✅ Assigned✅ Computed (counterfactual)❌ No
After Email/WorldID LoginSession created✅ Assigned❌ Not computed❌ No
After Email User Creates PasskeyPasskey linked✅ Same✅ Computed❌ No
User Receives TokensTokens sent to address✅ Same✅ Same❌ No (still works!)
First Outgoing TransactionSafe deployed✅ Same✅ SameYES

Database Records Created

When a user authenticates, the following database records are created/updated:

1. User Settings (shout_user_settings)

-- Created on first login
INSERT INTO shout_user_settings (
wallet_address, -- Spritz ID
wallet_type, -- 'eoa', 'passkey', 'email', 'world_id', 'alien_id'
created_at,
last_login
) VALUES (
'0x1234...', -- Or derived ID for non-wallet auth
'passkey',
NOW(),
NOW()
);

2. Passkey Credentials (shout_passkey_credentials)

-- Created when user registers a passkey
INSERT INTO shout_passkey_credentials (
user_address, -- Links to shout_user_settings
credential_id, -- Base64url credential ID from WebAuthn
public_key_x, -- P-256 X coordinate (hex)
public_key_y, -- P-256 Y coordinate (hex)
rp_id, -- "spritz.chat"
created_at
) VALUES (
'0x1234...',
'abc123...',
'0x8b4c2e3f...',
'0x1a2b3c4d...',
'spritz.chat',
NOW()
);

3. Session (shout_sessions)

-- Created on each login
INSERT INTO shout_sessions (
user_address,
token_hash, -- SHA-256 of JWT
auth_method, -- 'wallet', 'passkey', 'email', etc.
expires_at,
user_agent,
ip_address
) VALUES (
'0x1234...',
'sha256hash...',
'passkey',
NOW() + INTERVAL '7 days',
'Mozilla/5.0...',
'192.168.1.1'
);

API Endpoints by Flow

Wallet Login

EndpointMethodPurpose
/api/auth/verify?address=...GETGet SIWE message and nonce for signing
/api/auth/verifyPOSTVerify SIWE signature, create session
/api/wallet/smart-walletGETGet computed Smart Wallet address

Passkey Login

EndpointMethodPurpose
/api/passkey/register/optionsPOSTGet WebAuthn registration options
/api/passkey/register/verifyPOSTVerify registration, store credential
/api/passkey/login/optionsPOSTGet WebAuthn authentication options
/api/passkey/login/verifyPOSTVerify assertion, create session

Email Login

EndpointMethodPurpose
/api/email/send-codePOSTSend verification code
/api/email/verify-codePOSTVerify code, create session

Session Management

EndpointMethodPurpose
/api/auth/sessionGETGet current session info
/api/auth/logoutPOSTClear session, delete cookie

Implementation Examples

Check if User Has Wallet Access

import { useSmartWallet } from "@/hooks/useSmartWallet";

function WalletFeature() {
const { smartWallet, isLoading } = useSmartWallet(userAddress);

if (isLoading) return <Loading />;

// User logged in via email/WorldID without passkey
if (smartWallet?.needsPasskey) {
return <CreatePasskeyPrompt />;
}

// User has full wallet access
return (
<div>
<p>Wallet: {smartWallet?.smartWalletAddress}</p>
<p>Deployed: {smartWallet?.isDeployed ? "Yes" : "No"}</p>
<SendTokensButton />
</div>
);
}

Handle Different Auth Methods

async function handleLogin(method: AuthMethod) {
switch (method) {
case "wallet":
// Connect wallet → Sign SIWE → Verify
const address = await connectWallet();
const nonce = await getNonce();
const signature = await signSIWE(address, nonce);
await verifySignature(signature);
break;

case "passkey":
// Create/authenticate with passkey
const credential = await navigator.credentials.get({...});
await verifyPasskey(credential);
break;

case "email":
// Send code → Verify → Prompt for passkey later
await sendEmailCode(email);
await verifyEmailCode(email, code);
// User can now use social features
// Passkey prompt shown when wallet features needed
break;
}
}

Security Considerations

Session Security

PracticeImplementation
HTTP-only cookiesJWT stored in cookie, not accessible to JS
Secure flagCookie only sent over HTTPS
SameSite=StrictPrevents CSRF attacks
7-day expirySessions automatically expire

Passkey Security

RiskMitigation
PhishingPasskeys bound to domain (rpId = "spritz.chat")
Device lossRecovery signer can be added to Safe
Replay attacksEach signature includes unique challenge

Next Steps