Real-time messaging, video calls, livestreaming, and AI agents for Web3. Connect with friends using passkeys or wallets, chat via decentralized messaging, make HD video calls, go live with WebRTC streaming, and create custom AI agents.
Live at app.spritz.chat
- Custom AI Agents - Create personalized AI assistants with unique personalities
- Google Gemini Powered - Leverages Gemini 2.0 Flash for intelligent conversations
- Knowledge Base (RAG) - Add URLs to give agents domain-specific knowledge
- Web Search Grounding - Agents can search the web for real-time information
- x402 Micropayments - Monetize your agents with Coinbase's x402 protocol
- Agent Discovery - Explore public agents and share with friends
- Tags & Search - Tag agents for easy discovery
- Favorites - Star your favorite agents for quick access
- HD Video Calls - Real-time video and voice calls powered by Huddle01
- Decentralized Messaging - End-to-end encrypted chat via Logos Messaging (prev. Waku) protocols
- Group Calls - Multi-party video calls with friends
- Voice Messages - Record and send voice notes
- Push Notifications - Get notified of incoming calls and messages
- Link Previews - Rich previews for shared URLs
- Go Live - Broadcast live video to your friends with one tap
- WebRTC Streaming - Low-latency streaming powered by Livepeer
- Vertical Video - Optimized 9:16 portrait mode for mobile
- Real-time Viewer Count - See how many people are watching live
- Auto-Recording - Streams are automatically recorded for later playback
- HLS Playback - Viewers watch via adaptive HLS streaming
- Live Badge - Friends see when you're live on their dashboard
- Multi-Chain Support - Connect Ethereum, Base, Arbitrum, Optimism, Polygon, BNB Chain, Unichain, and Solana wallets
- SIWE/SIWS - Sign-In With Ethereum/Solana for secure authentication
- Passkey Authentication - Passwordless login using Face ID, Touch ID, or Windows Hello
- Multi-Wallet Support - Connect MetaMask, Coinbase Wallet, Phantom, and 300+ wallets
- ENS Integration - Resolve ENS names with live avatar preview
- Smart Accounts - ERC-4337 account abstraction with Safe (same address on all EVM chains)
- Non-Custodial - Your keys, your crypto. We never store private keys
- Passkey = Wallet Key - For non-wallet users, your passkey IS your wallet key (losing it means losing access)
- Passkey Signing - Sign transactions with Face ID, Touch ID, or Windows Hello
- Multi-Chain - Same wallet address on all 7 supported EVM chains
- Gas Sponsorship - Free transactions on L2s (Base, Arbitrum, Optimism, Polygon, BNB Chain, Unichain)
- ERC-20 Gas - Pay gas in USDC on Ethereum mainnet (no ETH needed)
- The Graph Integration - Real-time token balances and transaction history
- Trusted Tokens - Spam token filtering with curated whitelist
Wallet Architecture by Auth Type:
| Auth Method | Wallet Owner | How to Sign | Passkey Required? |
|---|---|---|---|
| EVM Wallet | Your wallet EOA | Connected wallet | ❌ No |
| Passkey | Passkey signer | Your passkey | ✅ Built-in |
| Passkey signer | Your passkey | ✅ Must create | |
| World ID | Passkey signer | Your passkey | ✅ Must create |
| Alien ID | Passkey signer | Your passkey | ✅ Must create |
| Solana | Passkey signer | Your passkey | ✅ Must create |
⚠️ Important for Email/Digital ID users: Your passkey controls your wallet. If you delete your passkey, you will lose access to any funds in your wallet. Use a synced passkey (iCloud Keychain, Google Password Manager) for backup.
- Friends System - Add friends, manage requests, and organize with tags
- Groups - Create and join group chats
- Pixel Art Avatars - Create custom 8-bit profile pictures
- Status Updates - Share what you're up to with friends
- QR Code Scanning - Quickly add friends by scanning their QR code
- Phone/Email Verification - Optionally verify your identity
- Social Links - Connect Twitter, Farcaster, and Lens profiles
- Google Calendar Sync - Connect your Google Calendar to sync availability
- Availability Windows - Set up recurring availability windows (like Calendly)
- Scheduling API - Coming soon: Schedule calls with others via AI agents or links
- x402 Payments - Coming soon: Charge for scheduled calls using x402
- Admin Dashboard - Manage users, invite codes, and permissions
- Analytics - Track usage metrics with beautiful charts
- Beta Access Control - Gate features for beta testers
- Points & Leaderboard - Gamification with daily rewards
- PWA Support - Install as a native app on iOS, Android, and desktop
- 3D Globe - Beautiful interactive globe visualization
- Dark Mode - Sleek dark UI throughout
- Mobile Optimized - Fully responsive design
- Censorship Resistance - Optional decentralized calling via Huddle01
| Category | Technology |
|---|---|
| Framework | Next.js 16 with App Router |
| Styling | Tailwind CSS 4 |
| Animations | Motion (Framer Motion) |
| 3D Graphics | Three.js with React Three Fiber |
| Web3 (EVM) | viem, wagmi, permissionless.js |
| Web3 (Solana) | @solana/wallet-adapter |
| Account Abstraction | Pimlico, Safe Smart Accounts (ERC-4337) |
| Token Data | The Graph Token API |
| Wallet Connection | Reown AppKit (WalletConnect) |
| Video Calls | Huddle01 SDK |
| Livestreaming | Livepeer (WebRTC/WHIP + HLS) |
| Messaging | Logos Messaging Protocols |
| AI/LLM | Google Gemini API |
| Vector Search | Supabase pgvector |
| Database | Supabase (Postgres + Realtime) |
| Push Notifications | Web Push API |
| Payments | x402 Protocol (Coinbase) |
- Node.js 18+
- npm (recommended) or yarn
- Supabase project
- Google Cloud account (for Gemini API)
- Clone the repository:
git clone https://github.com/Spritz-Labs/spritz.git
cd spritz- Install dependencies:
npm install- Set up environment variables:
cp .env.example .env.local-
Configure your environment variables (see Environment Variables)
-
Run database migrations (see Database Setup)
-
Start the development server:
npm run devOpen http://localhost:3000 in your browser.
# Supabase (Database & Realtime)
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
# WalletConnect / Reown
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your_project_id# Google Gemini (required for AI agents)
GOOGLE_GEMINI_API_KEY=your_gemini_api_key# Huddle01
NEXT_PUBLIC_HUDDLE01_PROJECT_ID=your_huddle01_project_id
HUDDLE01_API_KEY=your_huddle01_api_key# Livepeer
LIVEPEER_API_KEY=your_livepeer_api_key# Pimlico (ERC-4337 Bundler & Paymaster)
NEXT_PUBLIC_PIMLICO_API_KEY=your_pimlico_api_key
NEXT_PUBLIC_PIMLICO_SPONSORSHIP_POLICY_ID=sp_your_policy_id
# The Graph Token API (for balances & transactions)
GRAPH_TOKEN_API_KEY=your_graph_token_api_key
# Email Auth (Optional - for email login feature)
EMAIL_AUTH_SECRET=your_secure_secret_for_email_key_derivation# VAPID Keys (generate with web-push)
NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_vapid_public_key
VAPID_PRIVATE_KEY=your_vapid_private_key
VAPID_SUBJECT=mailto:your@email.com# Twilio
TWILIO_ACCOUNT_SID=your_twilio_account_sid
TWILIO_AUTH_TOKEN=your_twilio_auth_token
TWILIO_PHONE_NUMBER=your_twilio_phone_number
TWILIO_VERIFY_SERVICE_SID=your_verify_service_sid# Resend
RESEND_API_KEY=your_resend_api_key# Pinata (IPFS)
PINATA_API_KEY=your_pinata_api_key
PINATA_SECRET_KEY=your_pinata_secret_key
NEXT_PUBLIC_PINATA_GATEWAY=gateway.pinata.cloud# Helius RPC
NEXT_PUBLIC_HELIUS_API_KEY=your_helius_api_key# x402 Configuration
NEXT_PUBLIC_APP_URL=https://app.spritz.chat
X402_FACILITATOR_URL=https://x402.org/facilitator# Google Calendar OAuth
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URI=https://app.spritz.chat/api/calendar/callback- Go to Supabase Dashboard
- Create a new project
- Go to Settings → API
- Copy your Project URL, anon key, and service role key
- Go to Google AI Studio
- Click "Get API Key"
- Create a new API key
- Free tier: 15 RPM, 1,500 requests/day
- Go to Reown Cloud
- Create a new project
- Copy your Project ID
- Go to Pimlico Dashboard
- Create an account and project
- Copy your API key
- Create a sponsorship policy and copy the policy ID
- Fund your paymaster on each chain you want to sponsor:
| Chain | Sponsorship | Fund Paymaster? |
|---|---|---|
| Ethereum | User pays USDC | No |
| Base | Sponsored | Yes |
| Arbitrum | Sponsored | Yes |
| Optimism | Sponsored | Yes |
| Polygon | Sponsored | Yes |
| BNB Chain | Sponsored | Yes |
| Unichain | Sponsored | Yes |
- Go to The Graph Token API
- Create an API key
- Token API provides real-time balances and transaction history across all chains
- Go to Huddle01 Dashboard
- Create an account and project
- Copy your Project ID and API Key
- Go to Livepeer Studio
- Create an account
- Go to Developers → API Keys
- Create a new API key with Stream and Asset permissions
- Go to Google Cloud Console
- Create a new project or select an existing one
- Enable the Google Calendar API:
- Go to "APIs & Services" → "Library"
- Search for "Google Calendar API"
- Click "Enable"
- Create OAuth 2.0 credentials:
- Go to "APIs & Services" → "Credentials"
- Click "Create Credentials" → "OAuth client ID"
- Choose "Web application"
- Add authorized redirect URI:
https://app.spritz.chat/api/calendar/callback(or your domain) - Copy the Client ID and Client Secret
- Add the credentials to your
.envfile:GOOGLE_CLIENT_ID=your_client_id GOOGLE_CLIENT_SECRET=your_client_secret GOOGLE_REDIRECT_URI=https://app.spritz.chat/api/calendar/callback
Spritz uses Supabase with several tables. Run these migrations in your Supabase SQL editor:
-- Users table
CREATE TABLE shout_users (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
wallet_address TEXT UNIQUE NOT NULL,
username TEXT UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
last_login TIMESTAMPTZ,
is_admin BOOLEAN DEFAULT FALSE,
beta_access BOOLEAN DEFAULT FALSE,
-- Analytics
messages_sent INTEGER DEFAULT 0,
friends_count INTEGER DEFAULT 0,
voice_minutes NUMERIC DEFAULT 0,
video_minutes NUMERIC DEFAULT 0,
groups_joined INTEGER DEFAULT 0
);
-- Friends table
CREATE TABLE shout_friends (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_address TEXT NOT NULL,
friend_address TEXT NOT NULL,
tag TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_address, friend_address)
);
-- Friend requests
CREATE TABLE shout_friend_requests (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
from_address TEXT NOT NULL,
to_address TEXT NOT NULL,
status TEXT DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT NOW()
);-- Enable pgvector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Agents table
CREATE TABLE shout_agents (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
owner_address TEXT NOT NULL,
name TEXT NOT NULL,
personality TEXT,
system_instructions TEXT,
model TEXT DEFAULT 'gemini-2.0-flash',
avatar_emoji TEXT DEFAULT '🤖',
visibility TEXT DEFAULT 'private',
web_search_enabled BOOLEAN DEFAULT TRUE,
use_knowledge_base BOOLEAN DEFAULT TRUE,
message_count INTEGER DEFAULT 0,
tags JSONB DEFAULT '[]',
-- x402 configuration
x402_enabled BOOLEAN DEFAULT FALSE,
x402_price_cents INTEGER DEFAULT 1,
x402_network TEXT DEFAULT 'base-sepolia',
x402_wallet_address TEXT,
x402_pricing_mode TEXT DEFAULT 'global',
-- MCP & API tools
mcp_servers JSONB DEFAULT '[]',
api_tools JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Agent chat history
CREATE TABLE shout_agent_chats (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
agent_id UUID REFERENCES shout_agents(id) ON DELETE CASCADE,
user_address TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Knowledge base chunks with embeddings
CREATE TABLE shout_knowledge_chunks (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
agent_id UUID REFERENCES shout_agents(id) ON DELETE CASCADE,
knowledge_id UUID NOT NULL,
content TEXT NOT NULL,
embedding vector(768),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Create index for vector similarity search
CREATE INDEX ON shout_knowledge_chunks
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
-- Agent favorites
CREATE TABLE shout_agent_favorites (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_address TEXT NOT NULL,
agent_id UUID REFERENCES shout_agents(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_address, agent_id)
);-- Streams table
CREATE TABLE shout_streams (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_address TEXT NOT NULL,
stream_id TEXT NOT NULL, -- Livepeer stream ID
stream_key TEXT, -- Livepeer stream key (for WHIP)
playback_id TEXT, -- Livepeer playback ID
title TEXT,
description TEXT,
status TEXT DEFAULT 'idle', -- idle, live, ended
viewer_count INTEGER DEFAULT 0,
started_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Stream assets (recordings)
CREATE TABLE shout_stream_assets (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
stream_id UUID REFERENCES shout_streams(id) ON DELETE CASCADE,
user_address TEXT NOT NULL,
asset_id TEXT NOT NULL UNIQUE, -- Livepeer asset ID
playback_id TEXT,
playback_url TEXT,
download_url TEXT,
duration_seconds NUMERIC,
size_bytes BIGINT,
status TEXT DEFAULT 'processing', -- processing, ready, failed
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index for faster queries
CREATE INDEX idx_streams_user ON shout_streams(user_address);
CREATE INDEX idx_streams_status ON shout_streams(status);
CREATE INDEX idx_stream_assets_stream ON shout_stream_assets(stream_id);See the /migrations folder for complete migration scripts.
src/
├── app/
│ ├── api/
│ │ ├── admin/ # Admin endpoints
│ │ ├── agents/ # AI agent CRUD & chat
│ │ ├── auth/ # SIWE/SIWS verification
│ │ ├── huddle01/ # Video call rooms
│ │ ├── streams/ # Livestreaming API
│ │ ├── public/ # Public agent API (x402)
│ │ └── ...
│ ├── admin/ # Admin pages
│ └── page.tsx # Main app
├── components/
│ ├── AgentsSection.tsx # AI agents UI
│ ├── AgentChatModal.tsx # Agent chat interface
│ ├── CreateAgentModal.tsx
│ ├── EditAgentModal.tsx
│ ├── ExploreAgentsModal.tsx
│ ├── Dashboard.tsx # Main dashboard
│ ├── ChatModal.tsx # P2P chat
│ ├── VoiceCallUI.tsx # Video/voice calls
│ ├── GoLiveModal.tsx # Livestream broadcaster
│ ├── LiveStreamPlayer.tsx # Livestream viewer
│ └── ...
├── context/
│ ├── AuthProvider.tsx # SIWE/SIWS auth
│ ├── WakuProvider.tsx # Messaging (Logos Messaging)
│ └── Web3Provider.tsx # Wallet connection
├── hooks/
│ ├── useAgents.ts # Agent management
│ ├── useAuth.ts # Authentication
│ ├── useStreams.ts # Livestream management
│ ├── useBetaAccess.ts # Feature flags
│ ├── useSmartWallet.ts # Safe wallet address
│ ├── useSafePasskeySend.ts # Passkey transaction signing
│ ├── useWalletBalances.ts # Multi-chain balances
│ ├── useTransactionHistory.ts # Tx history
│ └── ...
├── lib/
│ ├── safeWallet.ts # Safe + Pimlico integration
│ ├── smartAccount.ts # Address calculation
│ ├── livepeer.ts # Livepeer API utils
│ └── x402.ts # x402 payment utils
└── config/
└── chains.ts # Supported chains config
Spritz supports multiple authentication methods, each providing a unique "Spritz ID" (identity address) used for social features. Users can also access the Spritz Wallet (a Safe Smart Account) for on-chain transactions.
Every user has two addresses:
| Address Type | Purpose | Stored In |
|---|---|---|
| Spritz ID | Identity for profile, friends, messages, username | shout_users.wallet_address |
| Spritz Wallet | Smart contract wallet for on-chain funds | Safe Smart Account (ERC-4337) |
| Method | Spritz ID Source | Wallet Owner | Has Wallet Immediately? |
|---|---|---|---|
| EVM Wallet | Wallet address (EOA) | Wallet EOA | ✅ Yes - wallet signs |
| Passkey | Derived from credential ID | Passkey signer | ✅ Yes - passkey signs |
| Existing account OR derived | Passkey signer | ❌ No - must create passkey first | |
| World ID | nullifier_hash from World ID |
Passkey signer | ❌ No - must create passkey first |
| Alien ID | alienAddress from Alien |
Passkey signer | ❌ No - must create passkey first |
| Solana | Solana wallet address | Passkey signer | ❌ No - must create passkey first |
Key Architecture Points:
- EVM Wallet users: Your connected wallet signs transactions directly. No passkey needed.
- Everyone else: You MUST create a passkey before you can receive/send tokens. Your passkey becomes your wallet key.
- Passkey = Wallet Access: For non-wallet users, the passkey IS the key to your funds. Losing your passkey means losing wallet access.
Authentication Flow:
User connects wallet via Reown AppKit
↓
Frontend requests SIWE (Sign-In With Ethereum) message
↓
User signs message with wallet
↓
Server verifies signature, creates session
↓
Spritz ID = wallet address (e.g., 0x1234...)
Spritz Wallet (Safe):
- Safe address derived from wallet address as owner
- Wallet signs Safe transactions directly
- No passkey needed - the connected wallet IS the signer
User Flow:
- Click "Connect Wallet"
- Select wallet (MetaMask, Coinbase, etc.)
- Sign SIWE message
- Full access to app + wallet features
Authentication Flow:
User clicks "Login with Passkey"
↓
Server generates authentication challenge
↓
Browser triggers WebAuthn ceremony
↓
User authenticates with biometric
↓
Server verifies credential, creates session
↓
Spritz ID = stored user_address from passkey_credentials table
Spritz Wallet (Safe):
- P256 public key extracted from passkey
- Safe WebAuthn Signer address calculated from public key
- Safe address derived from WebAuthn signer as owner
- Passkey signs all transactions via WebAuthn
New User Registration:
User clicks "Create Account"
↓
Server generates registration challenge
↓
Browser creates new passkey (WebAuthn)
↓
Server extracts P256 public key (x, y coordinates)
↓
Spritz ID = deterministic hash of credential ID
↓
Safe signer address calculated from public key
Key Storage:
passkey_credentials.credential_id- WebAuthn credential identifierpasskey_credentials.public_key_x/y- P256 coordinates for Safe signingpasskey_credentials.safe_signer_address- Precomputed WebAuthn signer
Authentication Flow:
User enters email address
↓
Server sends 6-digit verification code via Resend
↓
User enters code
↓
Server verifies code, checks for existing account:
IF email matches existing verified account:
→ Use that account's address (preserves profile!)
ELSE:
→ Derive new address from email + EMAIL_AUTH_SECRET
↓
Session created with final address
Spritz Wallet (Safe):
- Email users CANNOT sign EVM transactions directly
- Must register a passkey to use Spritz Wallet
- Once passkey registered, Safe uses passkey as signer
Backwards Compatibility:
- If user already has account with email (from any auth method)
- Email login finds and uses that existing account
- Prevents duplicate accounts when EMAIL_AUTH_SECRET changes
Authentication Flow:
User clicks "Sign in with World ID"
↓
World ID SDK opens verification
↓
User verifies with Orb/Device
↓
Server receives proof + nullifier_hash
↓
Server verifies proof with World ID API
↓
Spritz ID = nullifier_hash (unique per person per app)
Spritz Wallet (Safe):
- World ID users CANNOT sign EVM transactions
nullifier_hashis a proof identifier, not a real address- Must register passkey while logged in with World ID
- Passkey links to their World ID identity (nullifier_hash)
Identity Persistence:
nullifier_hashis deterministic per person per app- Same person always gets same Spritz ID
- Sybil-resistant: one person = one account
Authentication Flow:
User clicks "Sign in with Alien ID"
↓
Alien ID SDK opens verification
↓
User authenticates with Alien
↓
Server receives alienAddress
↓
Spritz ID = alienAddress
Spritz Wallet (Safe):
- Same as World ID - cannot sign EVM transactions
- Must register passkey to use Spritz Wallet
- Passkey links to their Alien ID address
Authentication Flow:
User connects Solana wallet
↓
Frontend requests SIWS (Sign-In With Solana) message
↓
User signs message with Solana wallet
↓
Server verifies signature
↓
Spritz ID = Solana address (base58 format)
Spritz Wallet (Safe):
- Solana wallets cannot sign EVM transactions
- Must register passkey for Spritz Wallet
- EVM funds stored in Safe on EVM chains
When a logged-in user registers a passkey:
User is logged in (World ID, Email, Wallet, etc.)
↓
Session contains their Spritz ID
↓
User clicks "Add Passkey" in Wallet settings
↓
Server checks getAuthenticatedUser()
↓
IF authenticated:
→ Passkey linked to EXISTING Spritz ID ✅
ELSE IF session cookie present but invalid:
→ REJECT: "Session expired, please log in again"
ELSE:
→ Create new account (for passkey-only registration)
Defensive Protections:
- If session exists → passkey links to existing account
- If session cookie present but expired → reject (prevents accidental new account)
- If userAddress matches existing account → link to it
- Only create new account if genuinely new user
Spritz uses Safe Smart Accounts with ERC-4337 (Account Abstraction):
┌─────────────────────────────────────────────────────────┐
│ Spritz Wallet │
├─────────────────────────────────────────────────────────┤
│ Safe Smart Account (same address on all EVM chains) │
│ ├── Owner: EOA address OR WebAuthn Signer │
│ ├── Bundler: Pimlico │
│ └── Paymaster: Sponsored (L2) or ERC-20 USDC (mainnet) │
└─────────────────────────────────────────────────────────┘
| Chain | Chain ID | Gas Payment | Sponsorship |
|---|---|---|---|
| Ethereum | 1 | ETH (or USDC if available) | User pays |
| Base | 8453 | Sponsored | Free |
| Arbitrum | 42161 | Sponsored | Free |
| Optimism | 10 | Sponsored | Free |
| Polygon | 137 | Sponsored | Free |
| BNB Chain | 56 | Sponsored | Free |
| Unichain | 130 | Sponsored | Free |
For Wallet Users (EOA signer):
safeAddress = calculateSafeAddress(walletAddress)
// Safe is owned by the user's EOAFor Passkey Users (WebAuthn signer):
webAuthnSignerAddress = calculateWebAuthnSignerAddress(publicKeyX, publicKeyY)
safeAddress = calculateSafeAddress(webAuthnSignerAddress)
// Safe is owned by the passkey's P256 signerYour Safe wallet address is deterministic and identical across all EVM chains. Send to any chain, funds are never lost - just on a different network at the same address.
1. User connects MetaMask
2. Signs SIWE message
3. Spritz ID = wallet address
4. Safe address calculated from wallet
5. User can send/receive immediately
(wallet signs Safe transactions)
1. User clicks "Create Account"
2. Creates passkey (Face ID/Touch ID)
3. Spritz ID = hash(credential_id)
4. Safe address calculated from passkey signer
5. User can send/receive immediately
(passkey signs Safe transactions)
1. User verifies with World ID
2. Spritz ID = nullifier_hash
3. User sees profile, can chat, add friends
4. User opens Wallet → "Register Passkey to Send"
5. Creates passkey (linked to nullifier_hash)
6. Safe address calculated from passkey signer
7. User can now send/receive
1. User logged in with Email/WorldID/etc.
2. Opens Wallet settings → "Add Passkey"
3. Creates passkey
4. Server detects existing session
5. Passkey linked to EXISTING Spritz ID
6. Safe uses new passkey as signer
7. Profile, friends, messages preserved ✅
- Wallet: Address never changes
- Passkey: Credential ID never changes
- Email: Finds existing account first, then derives
- World ID: Same nullifier_hash for same person
- Alien ID: Same address for same account
- Private keys never leave user's device
- Passkeys backed up via iCloud/Google automatically
- Server only stores public keys
- JWT sessions in HTTP-only cookies (7 days)
- Frontend tokens in localStorage (30 days, signed)
- CSRF protection via origin validation
Huddle01 provides decentralized video/voice calls with WebRTC.
User initiates call to friend
↓
Server calls Huddle01 API to create room
POST https://api.huddle01.com/api/v2/sdk/rooms/create-room
↓
Returns roomId (unique room identifier)
↓
Room shared with callee via push notification
User joins room (caller or callee)
↓
Server generates access token via Huddle01 SDK
↓
Token includes:
- roomId: The room to join
- role: HOST (full permissions)
- permissions: cam, mic, screen, data
- metadata: displayName, walletAddress
↓
Token signed with HUDDLE01_API_KEY
↓
Client uses token to connect to room
permissions: {
admin: true, // Can manage room
canConsume: true, // Can receive media
canProduce: true, // Can send media
canProduceSources: {
cam: true, // Camera access
mic: true, // Microphone access
screen: true, // Screen share
},
canRecvData: true, // Receive data messages
canSendData: true, // Send data messages
canUpdateMetadata: true,
}| Type | Description | Implementation |
|---|---|---|
| 1:1 Call | Direct call between two users | Single room, both as HOST |
| Group Call | Multi-party call | Single room, all as HOST |
| Voice Only | Audio without video | Camera disabled client-side |
- Room IDs are random UUIDs (unguessable)
- Tokens are short-lived JWTs
- Each participant gets their own token
- Wallet address embedded in metadata for identification
Spritz uses Logos Messaging protocols (formerly Waku) for decentralized, end-to-end encrypted messaging.
┌─────────────────────────────────────────────────────────┐
│ Message Flow │
├─────────────────────────────────────────────────────────┤
│ Sender │
│ ↓ │
│ Encrypt message with symmetric key (AES-GCM) │
│ ↓ │
│ Encode with Protobuf │
│ ↓ │
│ Publish to Waku network (content topic = conversation) │
│ ↓ │
│ Store encrypted copy in Supabase (backup) │
│ ↓ │
│ Receiver subscribes to content topic │
│ ↓ │
│ Decrypt with shared symmetric key │
└─────────────────────────────────────────────────────────┘
User logs in (any auth method)
↓
WakuProvider.initialize() called with userAddress
↓
Waku light node created (browser-based)
↓
Connects to Waku network peers
↓
Loads stored encryption keys from localStorage
↓
Subscribes to user's content topics
↓
Ready to send/receive messages
Spritz provides E2E encryption for all users regardless of authentication method. The encryption system uses ECDH (Elliptic Curve Diffie-Hellman) key exchange for secure key derivation.
How It Works:
| Auth Method | Spritz ID Source | E2E Encryption | Multi-Device |
|---|---|---|---|
| EVM Wallet | Wallet address (0x...) | ✅ ECDH | |
| Passkey | Hash of credential ID | ✅ ECDH | |
| Existing account OR derived | ✅ ECDH | ||
| World ID | nullifier_hash |
✅ ECDH | |
| Alien ID | alienAddress |
✅ ECDH | |
| Solana | Solana address (base58) | ✅ ECDH |
Key Insight: The encryption system doesn't care how you logged in—it only needs your Spritz ID. Since all auth methods produce a stable, unique identifier, E2E encryption works identically for everyone.
┌─────────────────────────────────────────────────────────────────┐
│ ECDH Key Exchange (Secure) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ User A User B │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Private Key (A) │ │ Private Key (B) │ │
│ │ Public Key (A) │────────────│ Public Key (B) │ │
│ └──────────────────┘ └──────────────────┘ │
│ │ │ │
│ └───────────┬───────────────────┘ │
│ ↓ │
│ Shared Secret = ECDH(A_private, B_public) │
│ = ECDH(B_private, A_public) ← Same result! │
│ ↓ │
│ Final Key = SHA256(shared_secret + context) │
│ ↓ │
│ Messages encrypted with AES-256-GCM │
│ │
└─────────────────────────────────────────────────────────────────┘
Security Improvement: Unlike the old deterministic approach where key = SHA256(addresses) (anyone could compute!), ECDH requires possession of a private key. Only the two conversation participants can derive the shared secret.
┌─────────────────────────────────────────────────────────────────┐
│ Key Storage Model │
├─────────────────────────────────────────────────────────────────┤
│ │
│ localStorage (device) Supabase (server) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ ECDH Private Key │ │ ECDH Public Key │ ← Public│
│ │ ECDH Public Key │ │ Encrypted Backup* │ ← Opt-in│
│ │ Encryption Key* │ └─────────────────────┘ │
│ └─────────────────────┘ │
│ │
│ * Only if user enables backup with PIN │
│ │
└─────────────────────────────────────────────────────────────────┘
- Each device has a different keypair
- Messages encrypted for one device won't decrypt on another
- Your public key in Supabase gets overwritten by the latest device
Solution: Enable Encryption Key Backup
Go to Settings → Privacy & Security → Message Encryption Key to:
- Create a 6-digit PIN
- Write down your 12-word recovery phrase
- Verify by entering 3 random words
- Your keypair is now backed up (encrypted with phrase + PIN)
On a new device:
- Go to Settings → Message Encryption Key → Restore
- Enter your 12-word phrase + PIN
- Keypair restored → all messages decryptable
For Direct Messages (DMs):
// ECDH key exchange (both users must have public keys registered)
const myKeypair = getOrCreateMessagingKeypair();
const peerPublicKey = fetchPeerPublicKey(peerAddress);
if (myPublicKeyInDb && peerPublicKey) {
// SECURE: ECDH key derivation
const sharedSecret = ECDH(myPrivateKey, peerPublicKey);
const symmetricKey = SHA256(sharedSecret + context);
} else {
// LEGACY FALLBACK: Deterministic (for backward compatibility)
const symmetricKey = SHA256("spritz-dm-key-v1:" + sortedAddresses);
}For Group Chats:
// Random symmetric key generated on group creation
const symmetricKey = generateSymmetricKey(); // 256-bit AES key
// Key stored in Supabase (TODO: distribute via encrypted envelopes)The chat UI shows encryption status:
- 🛡️ Green "Secure key exchange active" - Both users have ECDH keys
- 🔒 Amber "Encrypted (peer hasn't upgraded)" - Using legacy keys
- This helps users know when their conversation is fully secured
Messages are published to specific "content topics" based on conversation:
/spritz/1/dm-{sortedAddresses}/proto # Direct messages
/spritz/1/group-{groupId}/proto # Group messages
Messages are stored in multiple locations for reliability:
| Storage | Purpose | Encryption |
|---|---|---|
| Waku Network | Real-time delivery | Symmetric (AES-GCM) |
| Supabase | Backup/history | Symmetric (AES-GCM) |
| localStorage | Offline access | Symmetric (AES-GCM) |
message ChatMessage {
uint64 timestamp = 1; // Unix timestamp
string sender = 2; // Sender address
string content = 3; // Message text
string messageId = 4; // Unique ID (UUID)
string messageType = 5; // "text", "pixel_art", "system"
}Creating a Group:
User creates group with name, emoji, members
↓
Generate random groupId
↓
Generate random symmetric key
↓
Store group info in Supabase (shout_groups table)
↓
Encrypt group key for each member
↓
Members can decrypt key and join conversation
Group Invites:
Owner invites new member
↓
Encrypt symmetric key for new member
↓
Store encrypted key in shout_group_invites
↓
Invitee decrypts key and joins group
| Property | Implementation |
|---|---|
| End-to-End Encryption | AES-256-GCM symmetric encryption |
| Key Exchange | ECDH P-256 (replaces deterministic derivation) |
| Forward Secrecy | Not currently (would need ratcheting) |
| Key Storage | localStorage (opt-in encrypted backup) |
| Backup Protection | 12-word phrase + 6-digit PIN + PBKDF2 (100k iterations) |
| Message Authentication | GCM mode provides authentication |
| Sender Verification | Sender address in signed message |
| Feature | Security Level | Notes |
|---|---|---|
| DM Encryption | 🟢 Strong | ECDH key exchange, requires key possession |
| Group Encryption | 🟡 Moderate | Shared symmetric key in Supabase |
| Key Backup | 🟢 Strong | AES-GCM + PBKDF2, requires phrase + PIN |
| Multi-Device | 🟡 Requires Setup | Must backup/restore to sync keys |
| Legacy Compatibility | 🟡 Moderate | Falls back to deterministic keys if needed |
// Subscribe to conversation
streamMessages(peerAddress, (message) => {
// Decrypt and display new message
const decrypted = decrypt(message, symmetricKey);
addToConversation(decrypted);
});
// Also subscribe to Supabase realtime for backup delivery
supabase
.channel('messages')
.on('INSERT', handleNewMessage)
.subscribe();- Messages cached in localStorage
- On reconnect, sync from Supabase backup
- Deduplicate by messageId
- Merge with real-time Waku messages
- Click "Create Agent" in the Agents section
- Choose a name and personality
- Select visibility (private/friends/public)
- Optionally add tags for discovery
Add URLs to your agent's knowledge base:
- Open the agent's knowledge settings
- Add URLs (GitHub repos, documentation, web pages)
- Click "Index" to process the content
- The agent will use this knowledge in conversations
Enable x402 to charge for agent usage:
- Edit your agent's capabilities
- Enable x402 payments
- Set your price (in cents per message)
- Configure your wallet address
- Share the public API endpoint
External developers can integrate your agent using:
import { wrapFetch } from "x402-fetch";
const paidFetch = wrapFetch(fetch, wallet);
const response = await paidFetch(
"https://app.spritz.chat/api/public/agents/{id}/chat",
{
method: "POST",
body: JSON.stringify({ message: "Hello!" }),
}
);- Tap the "Go Live" button on your dashboard
- Allow camera and microphone access
- Add an optional title for your stream
- Tap "Go Live" to start broadcasting
- Share with friends - they'll see your live badge
- Friends who are live show a red "LIVE" badge on their avatar
- Tap their avatar to join the stream
- See real-time viewer count
- Streams auto-retry if connection drops
- Broadcast: WebRTC via WHIP protocol to Livepeer
- Playback: HLS adaptive streaming via Livepeer CDN
- Resolution: 1080x1920 (9:16 vertical/portrait)
- Recording: Automatic recording stored on Livepeer
Spritz works as a Progressive Web App:
- iOS: Tap Share → "Add to Home Screen"
- Android: Tap the install banner or Menu → "Install App"
- Desktop: Click the install icon in the address bar
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
PolyForm Noncommercial License 1.0.0
Commercial use requires a separate license. Contact connect@spritz.chat for commercial licensing.
Built with 🍊 by the Spritz team
Powered by Google Gemini, Huddle01, Livepeer, Logos Messaging, Supabase, Pimlico, Safe, The Graph, Reown, and x402