Skip to main content

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 use generateText with stopWhen: stepCountIs(10) for multi-step tool execution.

Routes

1. /api/sms/incoming (POST)

Purpose: Main webhook for incoming SMS messages from Twilio
Triggered 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 updates
Triggered 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 number
POST: Enables SMS (optIn: true only) and sends welcome SMS
Note: 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 database
How it works: Twilio adds OptOutType parameter to the normal incoming webhook
Action: Updates sms_opt_in column to match Twilio’s carrier-level blocking
Note: 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):
  1. Use generateText (not streamText)
  2. Use stopWhen: stepCountIs(10) for multi-step execution
  3. Extract response from result.response.messages[last]
  4. 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
How It Works:
  1. Twilio receives MMS with image(s)
  2. Twilio sends NumMedia, MediaUrl0, MediaContentType0 in webhook
  3. Our webhook downloads image from Twilio (requires auth)
  4. Convert image to base64
  5. Pass to Claude as multimodal content (text + image)
  6. Claude analyzes using vision capabilities
  7. Return concise SMS response
Technical Details:
  • 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_context JSONB column in logs

Files Created

1. lib/twilio.ts

Twilio utility functions
  • sendSMS(to, body) - Send a single SMS via Twilio API
  • sendLongSMS(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 1 prefix for 10-digit US numbers
  • splitMessage(text, maxLength) - Split long text into SMS-sized chunks
  • verifyTwilioSignature(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_users table by phone_number
  • Handles unknown users with friendly signup message
  • Uses generateText directly 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
Key features:
  • maxDuration = 60 seconds (Vercel timeout)
  • Uses Supabase service role key (bypasses RLS)
  • Stores fromNumber for 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_logs table 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
Indexes:
  • 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:
  1. Accept clerkId from request body (line 71)
  2. Support internal authentication (lines 124-130)
  3. Add SMS-specific system prompt (lines 245-265)
  4. 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:
    1. Twilio adds their number to a carrier-level blocklist
    2. Twilio sends automatic confirmation: “You have successfully been unsubscribed…”
    3. All future messages to that number fail with Error 21610
    4. Our webhook never sees this message
  • When user texts START or UNSTOP:
    1. Twilio removes them from blocklist
    2. Twilio sends automatic confirmation: “You have successfully been re-subscribed…”
    3. Messages can be sent again
What we DON’T control:
  • ❌ 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 Journey:
  1. User signs up → sms_opt_in = false (default)
  2. User goes to Settings → SMS Notifications → Checks box
  3. /api/sms/opt-in called → Updates sms_opt_in = true
  4. Welcome SMS sent: “Hi! SMS is now enabled. Text me anytime for instant insights…”
  5. User can now text Silvia → /api/sms/incoming checks sms_opt_in = true
Protection:

Syncing Both Layers (Advanced Opt-Out)

Problem: User texts STOP → 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:
  1. Enable Advanced Opt-Out in Twilio Console → Messaging Services
  2. ✅ No separate webhook needed! Twilio sends OptOutType to your existing incoming webhook
  3. When user texts STOP:
    • Twilio handles blocking + sends confirmation
    • Twilio calls /api/sms/incoming with extra parameter: OptOutType=STOP
    • We detect it and update: sms_opt_in = false
  4. When user texts START:
    • Twilio unblocks + sends confirmation
    • Twilio calls /api/sms/incoming with OptOutType=START
    • We detect it and update: sms_opt_in = true
Key Insight: According to Twilio’s documentation, the OptOutType parameter is sent to your existing Messaging Service webhook, not a separate endpoint.

Opt-In/Opt-Out Flow Diagram

Required elements (✅ We have all of these):
  1. ✅ Express written consent - Checkbox in settings with clear language
  2. ✅ Clear disclosure - “By checking this box, you agree to receive recurring messages…”
  3. ✅ Opt-out mechanism - Reply STOP + settings toggle
  4. ✅ Terms link - /sms-terms page with full T&Cs
  5. ✅ Rate disclosure - “Msg&Data rates may apply”
  6. ✅ Frequency disclosure - “Message frequency varies”
  7. ✅ Help keyword - Reply HELP (handled by Twilio)
SMS Settings Component Language:

Twilio Configuration

Phone Number Setup

  1. Go to Twilio Console → Phone Numbers → Manage → Active Numbers
  2. Click on your number: +1 (831) 215-4002
  3. Under “Messaging Configuration”:
    • A message comes in: Webhook
    • URL: https://www.cfosilvia.com/api/sms/incoming
    • HTTP Method: POST
  4. Under “Messaging Service”: Select “Silvia” (your messaging service)
  5. Under “Status Callback URL” (optional):
    • URL: https://www.cfosilvia.com/api/sms/delivery-status
    • HTTP Method: POST

Advanced Opt-Out Setup

  1. Go to Twilio Console → Messaging → Services → Silvia
  2. Navigate to Advanced Opt-Out Management
  3. Enable Advanced Opt-Out ✅
  4. ✅ No webhook configuration needed!
    • Twilio automatically sends OptOutType parameter to your existing webhook
    • The parameter is added to requests sent to: https://www.cfosilvia.com/api/sms/incoming
  5. When users text STOP/START/HELP, your incoming webhook will receive the OptOutType parameter

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

  1. Start dev server: npm run dev
  2. Start ngrok: ngrok http 3000
  3. Update .env: NEXT_PUBLIC_APP_URL=https://xxx.ngrok-free.app
  4. Restart dev server
  5. Update Twilio webhook to ngrok URL
  6. Send SMS to test

Production Testing

  1. Deploy to Vercel
  2. Add environment variables in Vercel dashboard
  3. Update Twilio webhook to production URL
  4. Complete A2P 10DLC registration (or use toll-free number)
  5. 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 - Uses sms_<MessageSid> as session ID ✅ Internal auth - Supports both Clerk auth and internal clerkId parameter

User Lookup Logic

Same as voice webhook for consistency:
  1. Extract phone from Twilio’s From field: +1 (608) 215-9565
  2. Normalize: Remove all non-digits → 16082159565
  3. Add country code if missing: 6082159565 → 16082159565
  4. Query database: SELECT * FROM all_users WHERE phone_number = '16082159565'
  5. If found: Process SMS with user context
  6. 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 3,450lastmonth.Top:Dining3,450 last month. Top: Dining 890, Shopping 650,Gas650, Gas 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.009permessage, 0.009 per message, ~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: 1.15/month(longcode)or1.15/month (long code) or 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

  1. Go to Twilio Console → Monitor → Logs → Messaging
  2. Filter by your phone number
  3. Check for delivery failures or errors

Check Supabase logs

Check Vercel logs

  1. Go to Vercel Dashboard → Your project → Logs
  2. Filter by /api/sms/incoming
  3. Look for errors or slow requests

Known Limitations

  1. A2P 10DLC required for production - Messages won’t deliver until registration is complete
  2. 160 character SMS limit - Long messages are split (costs more)
  3. No rich media in basic SMS - Would need MMS for images/charts
  4. Rate limits - Twilio has rate limits per number
  5. 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

  1. Code Reuse: SMS uses the same /api/chat endpoint as the web app
    • No duplicate AI logic
    • Same tools and capabilities
    • Consistent personality and responses
  2. Minimal Changes: Only added responseFormat flag and clerkId parameter
    • Existing chat system unchanged
    • Easy to maintain
    • Simple to extend
  3. Consistent User Experience: Same Silvia across all channels
    • Web chat
    • Voice calls (ElevenLabs)
    • SMS messages
    • Future: Email, WhatsApp, etc.
  4. 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_users table has phone_number column

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_logs table)
  • 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 (getFinancialMetrics tested)
  • Response generation: ✅ Working (concise, SMS-friendly)
  • Message sending: ✅ Working (queued in Twilio)
  • Message delivery: ⏳ Blocked until A2P registration complete

Next Steps

  1. Complete A2P 10DLC Registration in Twilio
    • Fill out brand information
    • Create campaign
    • Wait for approval (1-5 business days)
  2. Test with Real Users
    • Once A2P approved, test with multiple users
    • Verify message delivery
    • Test different queries (net worth, spending, holdings, etc.)
  3. 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_logs table
  • ✅ 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.