Problem Statement
Users can currently only type commands. Voice notes are a first-class WhatsApp media type, and speaking "deposit fifty dollars" is a meaningfully lower-friction interaction than typing it, especially for less text-comfortable users. This is a distinct feature from the existing text parser — it requires audio handling, transcription, and a materially higher tolerance for misrecognition on financial actions.
Current State
src/whatsapp/handler.ts currently processes text messages via Twilio's inbound webhook payload. There is no audio/media handling today. Intent parsing (src/nlp/parser.ts) is a closed union ('deposit' | 'withdraw' | 'balance' | 'earnings' | 'help' | 'unknown') resolved via a regex fast-path with a Claude Haiku fallback for anything the regex doesn't confidently match. Voice should feed into this exact same function — the value of this feature is entirely in getting from "audio" to "the same text a typed message would have produced," not in reimplementing intent handling for a new modality.
Proposed Solution
Integration Points
- Extend
src/whatsapp/handler.ts to detect incoming messages with audio media (Twilio's MediaUrl/MediaContentType fields on voice notes) and download the audio via the authenticated Twilio media URL.
- Add a speech-to-text step behind an explicit provider interface (e.g.
TranscriptionProvider with a single transcribe(audioBuffer): Promise<{ text: string; confidence: number }> method) so the actual STT vendor is swappable and not hard-coded into the handler — mirrors the existing pattern of abstracting the Stellar RPC client behind ResilientRpcClient rather than calling a specific provider's SDK directly from route handlers.
- Transcribed text is passed into the existing
parseIntent(message) in src/nlp/parser.ts unchanged — no new intent types, no parallel voice-specific parsing logic.
- Confirmation-before-execution: any parsed intent that would move funds or change strategy, when it originated from voice, must be echoed back to the user ("I heard: withdraw $50 — confirm?") and require an explicit affirmative reply before executing. This is a materially different UX requirement from typed commands (which are already an explicit, deliberate action) and should be implemented as an explicit flag threaded through from the voice entry point, not inferred implicitly.
- Low-confidence transcriptions (below a configurable threshold from the provider's confidence score) should produce a "please repeat or type your command" response rather than attempting to parse and guess.
Edge Cases & Failure Modes
- Transcription succeeds but produces nonsensical/unparseable text — falls through to the existing
'unknown' intent handling, same as a nonsensical typed message would.
- Transcription provider is down/errors — respond with a clear "voice messages aren't available right now, please type your command" rather than a silent failure or hang.
- Unsupported audio format/codec from a given device — detect and respond gracefully rather than passing an unsupported buffer into the transcription provider and erroring opaquely.
- User sends a voice note that says "yes" as the confirmation reply to a pending voice-originated action — confirmation replies should be accepted via either voice or text, since it would be inconsistent to require typing to confirm a spoken command.
Security & Privacy Considerations
- Audio content is more sensitive than text in some jurisdictions (biometric/voice-data regulations) — document what is and isn't retained (e.g. is the raw audio buffer discarded immediately after transcription, or stored?) and default to not persisting raw audio unless there's a specific reason to.
- The confirmation-before-execution requirement is itself a security control against misrecognition-driven unintended transactions — it must not be skippable or bypassable for any financial intent, and this should be covered by a specific test asserting a voice-originated deposit/withdraw intent never executes without a prior confirmation round-trip.
Out of Scope
- Real-time/streaming transcription — process complete voice notes only (matches how WhatsApp delivers them).
- Voice output (the agent replying with synthesized speech) — text/WhatsApp-formatted replies only, even for voice-originated requests.
Suggested Implementation Plan
TranscriptionProvider interface and one concrete implementation; audio detection/download in handler.ts.
- Wire transcribed text into the existing
parseIntent pipeline; low-confidence fallback response.
- Confirmation-before-execution flow for financial intents originating from voice.
- Fixture-based tests (clear command, ambiguous command, unsupported audio, provider failure).
Acceptance Criteria
Problem Statement
Users can currently only type commands. Voice notes are a first-class WhatsApp media type, and speaking "deposit fifty dollars" is a meaningfully lower-friction interaction than typing it, especially for less text-comfortable users. This is a distinct feature from the existing text parser — it requires audio handling, transcription, and a materially higher tolerance for misrecognition on financial actions.
Current State
src/whatsapp/handler.tscurrently processes text messages via Twilio's inbound webhook payload. There is no audio/media handling today. Intent parsing (src/nlp/parser.ts) is a closed union ('deposit' | 'withdraw' | 'balance' | 'earnings' | 'help' | 'unknown') resolved via a regex fast-path with a Claude Haiku fallback for anything the regex doesn't confidently match. Voice should feed into this exact same function — the value of this feature is entirely in getting from "audio" to "the same text a typed message would have produced," not in reimplementing intent handling for a new modality.Proposed Solution
Integration Points
src/whatsapp/handler.tsto detect incoming messages with audio media (Twilio'sMediaUrl/MediaContentTypefields on voice notes) and download the audio via the authenticated Twilio media URL.TranscriptionProviderwith a singletranscribe(audioBuffer): Promise<{ text: string; confidence: number }>method) so the actual STT vendor is swappable and not hard-coded into the handler — mirrors the existing pattern of abstracting the Stellar RPC client behindResilientRpcClientrather than calling a specific provider's SDK directly from route handlers.parseIntent(message)insrc/nlp/parser.tsunchanged — no new intent types, no parallel voice-specific parsing logic.Edge Cases & Failure Modes
'unknown'intent handling, same as a nonsensical typed message would.Security & Privacy Considerations
Out of Scope
Suggested Implementation Plan
TranscriptionProviderinterface and one concrete implementation; audio detection/download inhandler.ts.parseIntentpipeline; low-confidence fallback response.Acceptance Criteria
parseIntentfunction with no new parsing logic