SMS Implementation Summary
Overview
✅ Complete SMS/MMS integration for Silvia - Users can now text Silvia and get AI-powered financial insights via SMS, using the same AI and tools as the web app! ✅ Image Support (MMS) - Users can send images (receipts, charts, statements) and Claude will analyze them!Architecture
SMS uses the exact same pattern as the email integration - both usegenerateText with stopWhen: stepCountIs(10) for multi-step tool execution.
Routes
1. /api/sms/incoming (POST)
Purpose: Main webhook for incoming SMS messages from TwilioTriggered by: Twilio when user sends SMS to Silvia’s number
Security: Checks
sms_opt_in status before responding
2. /api/sms/delivery-status (POST)
Purpose: Webhook for SMS delivery status updatesTriggered by: Twilio when SMS delivery status changes (delivered, failed, etc.)
3. /api/sms/opt-in (GET/POST)
Purpose: Enable SMS notifications (opt-in only)GET: Returns current
sms_opt_in status and phone numberPOST: Enables SMS (
optIn: true only) and sends welcome SMSNote: To disable SMS, user must text STOP (enforced at Twilio carrier level)
4. Advanced Opt-Out (Built into /api/sms/incoming)
Purpose: Sync Twilio’s STOP/START keywords with our databaseHow it works: Twilio adds
OptOutType parameter to the normal incoming webhookAction: Updates
sms_opt_in column to match Twilio’s carrier-level blockingNote: No separate webhook needed - handled in the main incoming route
How It Works
Key Technical Details
AI SDK Version
- Required: AI SDK v5.0.60+ (updated from 5.0.15)
- Why: Fixed critical bug with Claude +
stopWhen+ tool calls - Before update: Claude would fail with “messages.1.content.0.tool_use.input: Input should be a valid dictionary”
- After update: Claude works perfectly with multi-step tool execution
Implementation Pattern
SMS follows the exact same pattern as email (/api/cron/check-emails/route.ts):
- Use
generateText(notstreamText) - Use
stopWhen: stepCountIs(10)for multi-step execution - Extract response from
result.response.messages[last] - Fallback to GPT-5 if Claude fails
Why Not Use /api/chat?
The webapp’s /api/chat uses streamText which returns a streaming response for the browser. SMS needs a complete response, so we use generateText directly (same as email).
MMS (Image) Support
Users can send images via MMS and Claude will analyze them! Supported Use Cases:- 📄 Receipts - “What’s in this receipt?” → Claude extracts merchant, amount, items
- 📊 Charts/Graphs - “Analyze this chart” → Claude describes trends and insights
- 📋 Statements - “What’s my balance?” → Claude reads account info
- 📈 Stock Charts - “Is this a good buy?” → Claude analyzes price action
- 💳 Credit Card Bills - “How much do I owe?” → Claude extracts balance
- Twilio receives MMS with image(s)
- Twilio sends
NumMedia,MediaUrl0,MediaContentType0in webhook - Our webhook downloads image from Twilio (requires auth)
- Convert image to base64
- Pass to Claude as multimodal content (text + image)
- Claude analyzes using vision capabilities
- Return concise SMS response
- Only processes images (skips videos/other media)
- Supports multiple images per message
- Uses Claude Sonnet 4.5’s vision capabilities
- Falls back to GPT-5 if Claude fails (GPT-5 also has vision)
- Image data stored in
user_contextJSONB column in logs
Files Created
1. lib/twilio.ts
Twilio utility functions
sendSMS(to, body)- Send a single SMS via Twilio APIsendLongSMS(to, body)- Split and send long messages (>1600 chars)normalizePhoneNumber(phone)- Convert phone to database format (digits only, with country code)- Example:
+1 (608) 215-9565→16082159565 - Adds
1prefix for 10-digit US numbers
- Example:
splitMessage(text, maxLength)- Split long text into SMS-sized chunksverifyTwilioSignature(signature, url, params)- Verify webhook authenticity (optional)
2. app/api/sms/incoming/route.ts
Main SMS/MMS webhook endpoint
- Receives POST requests from Twilio with form data
- Parses:
From,Body,MessageSid,To,NumMedia,MediaUrl0, etc. - Detects and downloads images from Twilio (MMS support)
- Normalizes phone number using same logic as voice webhook
- Looks up user in
all_userstable byphone_number - Handles unknown users with friendly signup message
- Uses
generateTextdirectly with multimodal content (text + images) - Includes SMS-specific system prompt with image analysis instructions
- Collects response and cleans markdown
- Sends response via Twilio (splits if needed)
- Logs all interactions in
silvia_sms_logs(with media URLs) - Error handling with user-friendly fallback messages
maxDuration = 60seconds (Vercel timeout)- Uses Supabase service role key (bypasses RLS)
- Stores
fromNumberfor error handling - Cleans response: removes
**,*,#, links, code blocks
3. app/api/sms/delivery-status/route.ts
Delivery status callback endpoint
- Receives POST requests from Twilio with delivery updates
- Parses:
MessageSid,MessageStatus,ErrorCode,ErrorMessage - Updates
silvia_sms_logstable with delivery status - Tracks:
queued,sending,sent,delivered,undelivered,failed - Logs delivery errors for debugging
4. sql-queries/create-sms-logs-table.sql
Database schema for SMS logging
clerk_id(for user queries)phone_number(for lookup)created_at(for time-based queries)twilio_message_sid(for delivery status updates)
5. sql-queries/add-delivery-status-columns.sql
Migration for existing tables
Adds delivery status tracking columns if table already exists.
Files Modified
app/api/chat/route.ts
Changes:
-
Accept
clerkIdfrom request body (line 71) -
Support internal authentication (lines 124-130)
-
Add SMS-specific system prompt (lines 245-265)
-
Conditional message processing (lines 337-351)
- For SMS: Uses manual message normalization (simple structure)
- For webapp: Uses AI SDK’s
convertToModelMessages(original behavior, unchanged) - This ensures SMS works without affecting webapp functionality at all
Environment Variables Required
Opt-In/Opt-Out System (Two-Layer Compliance)
Why Two Layers?
SMS opt-in/opt-out is handled by both Twilio and our application for comprehensive compliance:Layer 1: Twilio’s Automatic Opt-Out
How it works:- Twilio automatically handles keywords:
STOP,START,HELP,UNSTOP,QUIT,CANCEL, etc. - When user texts
STOP:- Twilio adds their number to a carrier-level blocklist
- Twilio sends automatic confirmation: “You have successfully been unsubscribed…”
- All future messages to that number fail with Error 21610
- Our webhook never sees this message
- When user texts
STARTorUNSTOP:- Twilio removes them from blocklist
- Twilio sends automatic confirmation: “You have successfully been re-subscribed…”
- Messages can be sent again
- ❌ Can’t customize STOP/START behavior for toll-free numbers
- ❌ Can’t prevent Twilio from blocking messages after STOP
- ❌ Our webhook doesn’t receive STOP/START messages (unless we enable Advanced Opt-Out)
Layer 2: Our Application’s Opt-In System
Database column:- User signs up →
sms_opt_in = false(default) - User goes to Settings → SMS Notifications → Checks box
/api/sms/opt-incalled → Updatessms_opt_in = true- Welcome SMS sent: “Hi! SMS is now enabled. Text me anytime for instant insights…”
- User can now text Silvia →
/api/sms/incomingcheckssms_opt_in = true
Syncing Both Layers (Advanced Opt-Out)
Problem: User textsSTOP → Twilio blocks them → Our database still shows sms_opt_in = true → User can’t re-enable in settings
Solution: Detect OptOutType parameter in /api/sms/incoming to sync Twilio’s actions with our database
How it works:
- Enable Advanced Opt-Out in Twilio Console → Messaging Services
- ✅ No separate webhook needed! Twilio sends
OptOutTypeto your existing incoming webhook - When user texts
STOP:- Twilio handles blocking + sends confirmation
- Twilio calls
/api/sms/incomingwith extra parameter:OptOutType=STOP - We detect it and update:
sms_opt_in = false
- When user texts
START:- Twilio unblocks + sends confirmation
- Twilio calls
/api/sms/incomingwithOptOutType=START - We detect it and update:
sms_opt_in = true
OptOutType parameter is sent to your existing Messaging Service webhook, not a separate endpoint.
Opt-In/Opt-Out Flow Diagram
Legal Compliance (TCPA)
Required elements (✅ We have all of these):- ✅ Express written consent - Checkbox in settings with clear language
- ✅ Clear disclosure - “By checking this box, you agree to receive recurring messages…”
- ✅ Opt-out mechanism - Reply STOP + settings toggle
- ✅ Terms link -
/sms-termspage with full T&Cs - ✅ Rate disclosure - “Msg&Data rates may apply”
- ✅ Frequency disclosure - “Message frequency varies”
- ✅ Help keyword - Reply HELP (handled by Twilio)
Twilio Configuration
Phone Number Setup
- Go to Twilio Console → Phone Numbers → Manage → Active Numbers
- Click on your number:
+1 (831) 215-4002 - Under “Messaging Configuration”:
- A message comes in: Webhook
- URL:
https://www.cfosilvia.com/api/sms/incoming - HTTP Method: POST
- Under “Messaging Service”: Select “Silvia” (your messaging service)
- Under “Status Callback URL” (optional):
- URL:
https://www.cfosilvia.com/api/sms/delivery-status - HTTP Method: POST
- URL:
Advanced Opt-Out Setup
- Go to Twilio Console → Messaging → Services → Silvia
- Navigate to Advanced Opt-Out Management
- Enable Advanced Opt-Out ✅
- ✅ No webhook configuration needed!
- Twilio automatically sends
OptOutTypeparameter to your existing webhook - The parameter is added to requests sent to:
https://www.cfosilvia.com/api/sms/incoming
- Twilio automatically sends
- When users text STOP/START/HELP, your incoming webhook will receive the
OptOutTypeparameter
A2P 10DLC Registration (Required for Production)
- Status: Completed ✅
- Required for: Sending SMS to US numbers from long codes
- Timeline: 1-3 business days for approval
- Approval: Campaign approved and active
Testing
Local Testing with ngrok
- Start dev server:
npm run dev - Start ngrok:
ngrok http 3000 - Update
.env:NEXT_PUBLIC_APP_URL=https://xxx.ngrok-free.app - Restart dev server
- Update Twilio webhook to ngrok URL
- Send SMS to test
Production Testing
- Deploy to Vercel
- Add environment variables in Vercel dashboard
- Update Twilio webhook to production URL
- Complete A2P 10DLC registration (or use toll-free number)
- Send SMS to test
Test Commands
Key Features
✅ Reuses existing chat system - Same Silvia, same tools, same personality ✅ SMS-optimized responses - Concise, plain text, no markdown ✅ User authentication - Only responds to registered users with phone numbers ✅ Phone number normalization - Consistent format matching voice webhook ✅ Message splitting - Automatically splits long responses into multiple SMS ✅ Full logging - Tracks all SMS interactions with delivery status ✅ Error handling - Graceful fallbacks for unknown users and API errors ✅ Delivery tracking - Optional callback for message delivery status ✅ Session tracking - Usessms_<MessageSid> as session ID
✅ Internal auth - Supports both Clerk auth and internal clerkId parameter
User Lookup Logic
Same as voice webhook for consistency:- Extract phone from Twilio’s
Fromfield:+1 (608) 215-9565 - Normalize: Remove all non-digits →
16082159565 - Add country code if missing:
6082159565→16082159565 - Query database:
SELECT * FROM all_users WHERE phone_number = '16082159565' - If found: Process SMS with user context
- If not found: Send signup message
Example Conversations
User: “What’s my net worth?” Silvia: “Your net worth is $1.2M (↑2.3% this month)” User: “How much did I spend last month?” Silvia: “You spent 890, Shopping 310” User: “Should I sell my AAPL stock?” Silvia: “That needs detailed analysis. I can look at your portfolio, AAPL performance, and market conditions. For full analysis, check the app at cfosilvia.com” Unknown user: “Hello” Silvia: “Hi! I’m Silvia, your personal CFO. I don’t recognize this number. Please sign up at cfosilvia.com to get started, and make sure to add your phone number in settings!”Cost Estimates
Per Message
- Twilio SMS: $0.0079 per segment (US)
- Claude API: ~$0.001 per response (varies by length)
- Total: ~0.018 per conversation
Monthly Estimates
- 100 conversations/day: ~$54/month
- 500 conversations/day: ~$270/month
- 1000 conversations/day: ~$540/month
Additional Costs
- Phone number: 2/month (toll-free)
- A2P 10DLC registration: One-time $4-15 fee
Database Queries
View recent SMS conversations
Check delivery failures
User SMS activity
Monitoring & Debugging
Check Twilio logs
- Go to Twilio Console → Monitor → Logs → Messaging
- Filter by your phone number
- Check for delivery failures or errors
Check Supabase logs
Check Vercel logs
- Go to Vercel Dashboard → Your project → Logs
- Filter by
/api/sms/incoming - Look for errors or slow requests
Known Limitations
- A2P 10DLC required for production - Messages won’t deliver until registration is complete
- 160 character SMS limit - Long messages are split (costs more)
- No rich media in basic SMS - Would need MMS for images/charts
- Rate limits - Twilio has rate limits per number
- Delivery delays - SMS can take 1-10 seconds to deliver
Next Steps
Immediate
- Complete SMS implementation
- Test locally with ngrok
- Complete A2P 10DLC registration in Twilio
- Deploy to production
- Test with real phone
Future Enhancements
- Add rate limiting (max 10 SMS/hour per user)
- Add proactive SMS notifications (e.g., “Your portfolio is down 5% today”)
- Add SMS analytics dashboard
- Add sentiment analysis for user messages
Architecture Benefits
Why This Approach Works
-
Code Reuse: SMS uses the same
/api/chatendpoint as the web app- No duplicate AI logic
- Same tools and capabilities
- Consistent personality and responses
-
Minimal Changes: Only added
responseFormatflag andclerkIdparameter- Existing chat system unchanged
- Easy to maintain
- Simple to extend
-
Consistent User Experience: Same Silvia across all channels
- Web chat
- Voice calls (ElevenLabs)
- SMS messages
- Future: Email, WhatsApp, etc.
-
Scalable: Built on serverless architecture
- Auto-scales with demand
- No infrastructure management
- Pay only for what you use
Troubleshooting
SMS not received
- Check A2P 10DLC registration status
- Verify Twilio webhook is configured correctly
- Check Twilio logs for delivery failures
- Ensure phone number is in E.164 format in database
Webhook errors
- Check Vercel logs for errors
- Verify environment variables are set
- Test webhook URL is accessible
- Check ngrok is running (for local testing)
User not found
- Verify phone number format in database matches normalized format
- Check
all_userstable hasphone_numbercolumn
Current Status
✅ Completed
- SMS webhook endpoint (
/api/sms/incoming) - Delivery status webhook (
/api/sms/delivery-status) - User lookup by phone number
- AI response generation with full tool access
- Multi-step tool execution (same as email)
- SMS-specific system prompt (concise, no markdown)
- Message splitting for long responses
- Comprehensive logging (
silvia_sms_logstable) - AI SDK updated to v5.0.60+ (fixed Claude bug)
- Claude + GPT-5 fallback working perfectly
⏳ Pending
- A2P 10DLC Registration - Required for SMS delivery in the US
- Brand registration
- Campaign registration
- Estimated time: 1-5 business days for approval
- Once approved, SMS will be delivered to users
🧪 Testing
- Local testing with ngrok: ✅ Working
- Tool execution: ✅ Working (
getFinancialMetricstested) - Response generation: ✅ Working (concise, SMS-friendly)
- Message sending: ✅ Working (queued in Twilio)
- Message delivery: ⏳ Blocked until A2P registration complete
Next Steps
-
Complete A2P 10DLC Registration in Twilio
- Fill out brand information
- Create campaign
- Wait for approval (1-5 business days)
-
Test with Real Users
- Once A2P approved, test with multiple users
- Verify message delivery
- Test different queries (net worth, spending, holdings, etc.)
-
Monitor and Optimize
- Monitor SMS logs in Supabase
- Track delivery status
- Optimize response conciseness if needed
- Ensure user has phone number set in their profile
Long response times
- Check Claude API latency
- Verify tool calls aren’t timing out
- Consider reducing system prompt length for SMS
- Check database query performance
Success Metrics
The implementation is working when:- ✅ Webhook returns
{"success":true} - ✅ User receives SMS response
- ✅ Messages logged in
silvia_sms_logstable - ✅ Response is concise and SMS-friendly
- ✅ Unknown users get signup message
- ✅ Delivery status updates received (if configured)
Status: ✅ Implementation complete and tested locally. Ready for A2P 10DLC registration and production deployment.