Spritz is a decentralized, permission-less and censorship resistant chat app for web3. Connect with friends using passkeys or wallets. Make HD video calls, go live to your followers, and chat freely on a decentralized network.
spritz is a growing TypeScript project in the AI payments / x402 ecosystem, focused on ai, chat, messaging, social-media. It currently has 20 GitHub stars and 6 forks, and sits alongside related tools like dna-x402, prismix, CrawlPay, GhostSpeak, x402Nano-API, bounties.
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
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.
| 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) |
git clone https://github.com/Spritz-Labs/spritz.git
cd spritz
npm install
cp .env.example .env.local
Configure your environment variables (see Environment Variables)
Run database migrations (see Database Setup)
Start the development server:
npm run dev
Open 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
| 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 |
https://app.spritz.chat/api/calendar/callback (or your domain).env file: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:
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):
User Flow:
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):
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 signerAuthentication 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):
Backwards Compatibility:
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):
nullifier_hash is a proof identifier, not a real addressIdentity Persistence:
nullifier_hash is deterministic per person per appAuthentication 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):
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):
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:
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 EOA
For Passkey Users (WebAuthn signer):
webAuthnSignerAddress = calculateWebAuthnSignerAddress(publicKeyX, publicKeyY)
safeAddress = calculateSafeAddress(webAuthnSignerAddress)
// Safe is owned by the passkey's P256 signer
Your 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 ✅
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 |
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 | ⚠️ Backup needed |
| Passkey | Hash of credential ID | ✅ ECDH | ⚠️ Backup needed |
| Existing account OR derived | ✅ ECDH | ⚠️ Backup needed | |
| World ID | nullifier_hash |
✅ ECDH | ⚠️ Backup needed |
| Alien ID | alienAddress |
✅ ECDH | ⚠️ Backup needed |
| Solana | Solana address (base58) | ✅ ECDH | ⚠️ Backup needed |
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 │
│ │
└─────────────────────────────────────────────────────────────────┘
⚠️ Important: ECDH keypairs are generated per device. Without backup:
Solution: Enable Encryption Key Backup
Go to Settings → Privacy & Security → Message Encryption Key to:
On a new device:
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:
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();
Add URLs to your agent's knowledge base:
Enable x402 to charge for agent usage:
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!" }),
}
);
Spritz works as a Progressive Web App:
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
DNA — Payment rails for AI agents. x402 micropayment protocol on Solana. Netting, transfer, stream settlement. Receipt anchoring on-chain.
A beautiful, powerful, and easy-to-use open-source AI gateway. Native cryptocurrency support.
Monetize your site for the AI era. AI bots pay $0.001 USDC per page via Arc Nanopayments.
AI Agent Commerce Protocol on Solana - enabling autonomous agents to trade services and exchange value on Solana blockchain with sub-second finality
💳 Streamline your Nano cryptocurrency transactions with our flexible payment gateway API, designed for easy wallet management and transaction handling.
The Sovereign Hunter: A collection of production-grade AI agent and full-stack solutions built to solve the world's highest-value bounties. From x402 autonomous payment systems to deep-tech analytics, this repository showcases the combined power of the Ai Indigo Group. Built for profit, performance, and the future of decentralized problem-solving.
The agent-native LLM router for autonomous agents. 55+ models (8 free), <1ms local routing, USDC payments on Base & Solana via x402.
A payments protocol for the internet. Built on HTTP.
A local-first AI agent with persistent memory, emotional intelligence, and a peer-to-peer skills economy.
The trust layer for agent-to-agent commerce — natural-language mandates, ERC-7710 delegated permissions, x402 payments, escrow, and dispute resolution as one open, catch-all Agent Skill / Claude Code plugin.
Self-healing infrastructure for AI agent payments. 90.3% auto-recovery.
The AI agent with a wallet — spends USDC autonomously to get real work done. Apache-2.0, TypeScript.