From
Snow­plows
to Platform

How one engineer built a voice-first contractor marketplace from scratch — twice — using AI-agentic coding and real customer feedback from snowstorms.

0
Engineer
0
Phone Number
0
Service Categories
0
Days of Development
Scroll
01
A snowstorm, a phone number, and a bet

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.

02
Learning in production

Every improvement came from a real customer, a real driver, or a real bug. Not from planning — from doing.

Jan 12-15

Remote operations go live

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.

Jan 16

First real job through the full funnel

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.

Jan 17

11 commits overnight: the photo sprint

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.

Jan 19

Voice AI meets accents

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.

Jan 21

Production launch + guerilla marketing

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.

Jan 22-25

The infrastructure crisis

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).

Jan 25-28

TypeScript/DDD migration

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.

Teams of 4 shovelers splitting $10 per driveway = $2.50 per person. They'd gladly take $30+ dispatched jobs if someone could send them leads.
Field observation — Toronto snowstorm, Jan 28
03
From vertical MVP to platform

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.

EcoPlow

  • Single service (snow plowing)
  • Single city (London, ON)
  • First-come-first-served dispatch
  • Flat rate pricing ($10)
  • Monolithic Express.js
  • Vapi for voice AI
  • 3,000-line server.js

CallOne

  • 14 service categories
  • 38 subdivisions, expandable
  • Competitive auction bidding
  • Credit-based revenue model
  • NestJS Clean Architecture
  • ElevenLabs conversational AI
  • 977 TypeScript source files
04
9 phases in 6 days

A comprehensive architecture plan was written before any code. Then every phase was built, tested, and verified before advancing to the next.

01 Foundation
  • Turborepo + Bun monorepo (API, Web, MCP Server)
  • Database schema with UUID v7 generator
  • 18+ domain enums, 4 value objects with validation
  • Geographic seed data: 6 regions, 38 subdivisions, 14 service categories
  • CI pipeline with GitHub Actions
02 Authentication & Users
  • User/Customer/Contractor entities with full lifecycle
  • Magic link auth (7-day expiry) + JWT with refresh rotation
  • Contractor tiers: Bronze → Silver → Gold → Platinum
  • Certification tracking (WSIB, insurance, trade licenses)
  • Role-based guards and rate limiting
03 Voice Integration
  • 3 ElevenLabs AI agents: Router, Customer Qualification, Contractor Signup
  • Twilio webhook handlers for calls and SMS
  • Conversation webhook → automated job/contractor creation
  • Returning customer recognition via phone number lookup
  • SMS confirmations after call completion
04 Job & Auction System
  • Job lifecycle: Draft → PendingAuction → AuctionActive → Assigned → Completed
  • Auction state machine with urgency-based timers (15min to 24hr)
  • Geographic contractor matching with radius expansion
  • Countdown starts on first bid (not on creation)
  • No-bid admin flagging and intervention
05 Payments & Credits
  • Stripe integration behind PaymentGatewayPort
  • Contractor wallets with credit balance tracking
  • Async payment: bid → pending transaction → Stripe webhook confirms
  • Promotion system with configurable bonuses
  • 12 payment use cases covering all credit flows
06 Real-time & Notifications
  • Supabase Realtime for auction updates
  • GCP Pub/Sub for contractor fan-out
  • Firebase Cloud Messaging (Android/iOS/Web)
  • Tiered notification delay (Platinum first)
  • 6 notification adapters behind port interfaces
07 Contractor UI & Customer Portal
  • 17 route pages, 49 components, 50+ API client functions
  • Job browsing, auction countdown, bid placement
  • Wallet management with Stripe Checkout
  • Customer magic link: edit job, upload photos, track status
  • SMS as a full interface (contractors can do everything via text)
08 Admin Dashboard
  • 96 files, 4,088 lines — largest single phase
  • Dashboard metrics, contractor management, auction intervention
  • Promotion CRUD, wallet adjustments, audit log
  • Reusable component library (DataTable, Pagination, StatCard)
09 MCP Server, Testing & Production
  • MCP server auto-generated from OpenAPI spec
  • 713 tests across 148 files (80% coverage target)
  • Security hardening: Helmet, CORS, rate limiting, 3 webhook guards
  • Environment validation at startup (fail fast)
  • Structured logging, global exception filter, health checks
05
Making it work with real callers

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.

Jan 31 – Feb 1

The deployment gauntlet — 7 Dockerfile fixes

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).

Feb 2-3

SMS conversations with ElevenLabs text AI

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.

Feb 4-5

SMS conversations go live

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.

Feb 5

E2E testing infrastructure

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.

Feb 10

Real-time voice relay

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.

Feb 11

Database ACID + Row Level Security

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.

Feb 12

Voice agent configuration as code

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.

Feb 12-13

Voice call hardening — 6 commits, 6 bugs

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.

Feb 14-17

Gemini AI analysis + geocoding + E2E verification

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.

Feb 24

CQ agent consistency — name extraction + prompt hardening

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.

Every staging test call found 2-3 new bugs. Mock tests pass; production is different. You can't design for accents, background noise, and "King Louis Crescent" from an armchair.
Engineering observation — Voice call hardening sprint, Feb 12
5 more phases in 11 days

After the initial build, every phase was driven by real staging test calls. Each call surfaced new edge cases that shaped the next iteration.

10 Real-Time Voice Infrastructure
  • WebSocket relay bridging Twilio Media Streams ↔ ElevenLabs audio
  • Inline post-call processing (polls ElevenLabs API on close, no webhook needed)
  • Transfer sound masking with typing audio during agent handoffs
  • Audio format correction and caller phone number injection
  • Seamless voice transfers — no re-greetings, same-person continuity
11 SMS Conversation Engine
  • Multi-turn SMS conversations via ElevenLabs WebSocket text API
  • Router detects customer vs contractor from first message keywords
  • Guidance context injection tells AI what fields are still missing
  • Cross-channel carry-forward: voice → SMS session continuity
  • Qualification gate: 7 universal + trade-specific fields before job creation
12 Data Extraction Pipeline
  • Backend keyword extraction: 14 service categories, 38 cities, urgency, names, addresses
  • Three-layer address extraction: numeric prefix, street-suffix fallback, context-aware
  • Spoken number normalization: "twenty two" → "22" for voice transcripts
  • Gemini structured transcript analysis: AI overrides keywords on disagreement
  • Google Maps geocoding with postal code, precision, and street name validation
13 Agent Config as Code
  • Prompts as Markdown + workflow configs as JSON, version-controlled in repo
  • Sync script: push/pull/diff with ElevenLabs API, runs in CI
  • Branch-aware deployment for staging ↔ production isolation
  • Structured workflows: greeting → intake → identity_and_confirm
  • LLM upgrade to Gemini 2.5 Flash for lower latency
14 E2E Testing & Security
  • E2E test visualizer: customer SMS panel, contractor notifications, data tracking
  • Backend test endpoints: send-sms, captured messages, session status, reset
  • Database transactions (ACID) for bid placement and auction closing
  • Row Level Security on all 20+ tables with proper policies
  • Contractor onboarding: 5-step wizard with certifications and password setup
1,050+
Source Files
800+
Tests Passing
80+
Use Cases
120+
Commits
14
Phases Built
12s
Test Suite
Built with
NestJS
11.0
Next.js
15.1
React
19.0
TypeScript
5.7
Bun
1.3
Tailwind
4.0
Supabase
PostgreSQL
Stripe
20.3
Twilio
5.4
ElevenLabs
Conversational AI
Firebase
13.6
GCP
Cloud Run
Gemini
2.5 Flash
Google Maps
Geocoding API
Claude Code
Opus 4.6
WebSocket
Media Streams
05
AI-agentic coding in practice

The entire CallOne platform was built using Claude Code as the primary development tool. Here's what that actually looked like.

01

Manager/Sub-Agent Pattern

A manager agent maintains the full plan. Sub-agents get fresh context per phase. Gates verify before advancing. Retrospectives capture learnings.

02

Architecture Before Code

Full architecture plan written and reviewed before Phase 1. Every decision documented with rationale and tradeoffs.

03

Mock System From Day One

Every external service has a mock adapter. 713 tests run in 12 seconds with zero API keys, zero costs, zero flakiness.

04

MCP Integration

Supabase, ElevenLabs, and the app's own API exposed as MCP tools. AI can query the database or call endpoints directly.

05

CLAUDE.md as Context

Project structure, commands, environments, deployment details in one file. Reduces context repetition across sessions dramatically.

06

Phase-Gated Execution

Each phase had verification criteria. Tests pass before advancing. Later phases can't break earlier ones.

06
What we learned
01

Ship early, learn from real calls

Every voice AI improvement came from a real conversation. You can't design for accents, background noise, and salespeople from an armchair.

02

The mock pattern is essential

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.

03

Staging isolation is non-negotiable

Learned the hard way: staging wrote to production. On v1, Supabase branching + environment-specific configs were table stakes from Phase 1.

04

AI coding tools are multipliers, not replacements

Claude Code built 42,000 lines in 6 days. But architecture decisions, market validation, and voice agent tuning all required human judgment.

05

Every phase builds on the last

Clean architecture and comprehensive tests aren't overhead — they're what made it possible to ship 9 phases in 6 days without regressions.

06

Document as you go

Architecture plans, CLAUDE.md, and journals weren't afterthoughts. They were the context that made each session productive.

07

EcoPlow teaches you what CallOne needs

EcoPlow's monolith, staging contamination, and voice agent iterations directly informed CallOne's architecture. The rewrite wasn't waste — it was applied learning.

08

Real calls break everything mock tests miss

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.

09

AI extraction needs a second opinion

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.

10

Never trust geocoding results blindly

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.