How one engineer built a voice-first contractor marketplace from scratch — twice — using AI-agentic coding and real customer feedback from snowstorms.
January 2026. Toronto gets buried in snow. The idea was simple: what if a homeowner could call one phone number and get their driveway plowed? No app, no browsing, no forms. Just talk to an AI, and a driver shows up.
The first version was called EcoPlow — a Vapi voice agent, Twilio SMS, a monolithic Express.js backend, and a 3,000-line server.js file. It worked. Barely. But it worked.
Every improvement came from a real customer, a real driver, or a real bug. Not from planning — from doing.
Operating a local snow clearing service entirely remotely from 200km away. Vapi voice agent handles inbound calls, Twilio SMS dispatches drivers, a Next.js dashboard tracks everything. Onboarded the first driver via phone call — he signed up, got verified, and was dispatched his first job all through voice and text. Launched Facebook ads targeting London, ON homeowners.
Google Ads → AI voice booking (77 seconds) → SMS driver dispatch → Driver A accepts but his plow can’t fit in the driveway corners — he cancels. The system returns the job to the pool but only notifies the customer, not other drivers. Driver B finds the re-available job by chance on the web dashboard. One job, two bugs found, a feature gap exposed (driver equipment matching), and the entire business model validated — all in one afternoon.
Driver feedback was immediate: “I showed up with zero context. I didn’t know what the driveway looked like.” That night, 11 commits: customer photo upload via SMS link with client-side compression, multi-select driveway types (base types mutually exclusive, modifiers combinable), driver completion photos for proof of work, a dedicated driver-view page with before/after photos, authorization security fixes, and a photo cleanup cron for storage costs.
A customer said “please, the driveway” with a thick accent. Speech-to-text transcribed it as “pizza drivers.” The AI triggered its off-topic handler and tried to end the call. Three fixes from that single conversation: clarify-before-dismissing logic (“Sorry, I didn’t catch that”), proactive call ending after booking confirmation, and background noise awareness so post-booking chatter doesn’t trigger new requests. Over 8 phases, the voice agent evolved from a naive prompt to a production system — every phase triggered by a real call, not a test scenario.
28 commits merged to production. Built a /release Claude skill for guided deployments. Facebook ads weren’t converting, so pivoted to guerilla marketing: walking Toronto neighborhoods during a massive snowstorm, handing business cards to shoveling crews knee-deep in snow. Critical market insight: teams of 4 shovelers splitting $10 per driveway = $2.50 per person. They had no lead generation — just walking door to door. They’d gladly take $30+ dispatched jobs through a platform. The supply side was as underserved as the demand side.
Post-launch log analysis revealed the worst kind of bug: staging and production shared the same database. Test jobs appearing in production. SMS tests going to real customers. Replying “ACCEPT” to a staging SMS could mutate a production job. Six cross-contamination bugs found. Built complete isolation: Supabase branching, Row Level Security, environment column on every query, SMS whitelist for staging, Vapi tool isolation. Then built a CI pipeline with 10 parallel test suites including Vapi voice agent behavior evaluations (100% pass rate on 8 core scenarios).
The monolithic 3,000-line server.js was unmaintainable. 94 commits over 6 days. 172 files changed. Complete rewrite to TypeScript with domain-driven design: value objects (Money, Phone, Address, Rating), entities (Job, Driver), Supabase repositories, service orchestration, and clean route handlers. The codebase went from untestable monolith to properly layered architecture.
EcoPlow proved the model: AI voice agent → job creation → contractor dispatch → payment. But it was built for one service in one city. The bigger opportunity was any home service — plumbing, HVAC, electrical, roofing — across any geography.
This required a fundamentally different architecture. EcoPlow was a vertical MVP. CallOne needed to be a platform.
A comprehensive architecture plan was written before any code. Then every phase was built, tested, and verified before advancing to the next.
The platform was built. Now it needed to survive real phone calls, real SMS conversations, and real voice transcripts with accents, background noise, and ambiguous addresses.
Deploying a Bun monorepo to Cloud Run was a journey of its own. Seven Dockerfile iterations: frozen lockfile, copying ALL workspace package.jsons for lockfile integrity, preserving bin symlinks across stages, keeping the full node_modules layout in the runner, adding tsc-alias for path alias resolution, mock adapters for missing secrets, and migrating from deprecated GCR to Artifact Registry. Bun’s workspace hoisting means you can’t just copy one app’s node_modules — the entire workspace structure must be intact. GCP auth evolved from service account keys (insecure) to Workload Identity Federation (keyless).
Built a full SMS-based conversational AI flow: customer texts in, ElevenLabs text AI qualifies the job via back-and-forth SMS, job created automatically with a magic link to confirm. Three WebSocket bugs debugged: the agent sends a greeting automatically (had to skip this turn), agent_response events fire for the greeting too (gated behind a response turn flag), and user_message must wait until greeting completes. Added ElevenLabs agent branching (like git branches for AI agents) and cross-channel session context — start a conversation via SMS, continue it on a phone call.
Built multi-turn SMS qualification using ElevenLabs text AI via WebSocket. Each SMS turn opens a new connection with conversation history as context. Router detects customer vs contractor intent. Cross-channel carry-forward from voice to SMS.
Built a full E2E test visualizer: customer panel for sending SMS, contractor panel for notifications, extracted data tracking, auction close controls. Backend data extraction with keyword matching against 14 service categories, 38 cities, urgency levels, and trade-specific fields.
Replaced webhook-based voice processing with a live WebSocket relay. Twilio Media Streams ↔ ElevenLabs Conversational AI, bridged in real-time. Sub-second latency. Inline post-call processing on WebSocket close — no webhook dependency.
Built a TransactionRunner for Supabase. Wrapped bid placement and auction closing in database transactions. Enabled Row Level Security on all 20+ tables. No more race conditions on concurrent bids.
Built config-aware ElevenLabs sync: prompts as Markdown, workflow configs as JSON, all version-controlled. CI auto-pushes on every deploy. Branch-aware for staging isolation. Structured workflows with greeting → intake → confirmation nodes.
Every staging test call found new issues. LLM latency (upgraded to Gemini 2.5 Flash). Transfer loops. Call cutoff at "finding you a contractor." White noise from wrong audio codec. Double-complete race condition. Branch routing silently dropped by ElevenLabs API.
Added Gemini as a structured transcript analyzer — one API call extracts description, service category, address, and city. Validates against keyword extraction, overrides only on disagreement. Google Maps geocoding validates postal codes, location precision, and street name matching. Upgraded to Gemini 2.5 Flash with thinking model response filtering. Fixed name extraction false positives. Full E2E verified on staging: voice → SMS → job creation → Gemini summaries → photo upload → contractor match.
Fixed “Twenty” bug where number words were extracted as customer names. Strengthened Phase 2/3 checklist enforcement so the CQ agent always collects name and city before wrapping up. Disabled LLM thinking for latency, lowered temperature to 0.1. Created ElevenLabs simulation tests and documented a known-good agent baseline for rollback safety.
After the initial build, every phase was driven by real staging test calls. Each call surfaced new edge cases that shaped the next iteration.
The entire CallOne platform was built using Claude Code as the primary development tool. Here's what that actually looked like.
A manager agent maintains the full plan. Sub-agents get fresh context per phase. Gates verify before advancing. Retrospectives capture learnings.
Full architecture plan written and reviewed before Phase 1. Every decision documented with rationale and tradeoffs.
Every external service has a mock adapter. 713 tests run in 12 seconds with zero API keys, zero costs, zero flakiness.
Supabase, ElevenLabs, and the app's own API exposed as MCP tools. AI can query the database or call endpoints directly.
Project structure, commands, environments, deployment details in one file. Reduces context repetition across sessions dramatically.
Each phase had verification criteria. Tests pass before advancing. Later phases can't break earlier ones.
Every voice AI improvement came from a real conversation. You can't design for accents, background noise, and salespeople from an armchair.
From day one, every external service had a mock adapter. This meant 713 tests running in 12 seconds with zero API keys and zero flakiness.
Learned the hard way: staging wrote to production. On v1, Supabase branching + environment-specific configs were table stakes from Phase 1.
Claude Code built 42,000 lines in 6 days. But architecture decisions, market validation, and voice agent tuning all required human judgment.
Clean architecture and comprehensive tests aren't overhead — they're what made it possible to ship 9 phases in 6 days without regressions.
Architecture plans, CLAUDE.md, and journals weren't afterthoughts. They were the context that made each session productive.
EcoPlow's monolith, staging contamination, and voice agent iterations directly informed CallOne's architecture. The rewrite wasn't waste — it was applied learning.
Every staging voice call found 2-3 new bugs. Accents, background noise, "twenty-two" vs "22", city names embedded in street names. You need real callers to find real problems.
Keyword matching alone misclassifies ambiguous cases. "Refrigerator isn't working" could be electrical or appliance-repair. Gemini as a structured analyzer catches what regex can't.
Google Maps returned a confident result for "King Louis Crescent" with a truncated 3-digit postal code. Always validate: postal format, location precision, and street name matching.