SoW analysis · technical review · delivery plan
Gateway
A full teardown of the SoW, a deep technical analysis of what it really demands — then how we would build it, who builds it, and by when.
What the analysis found
Buildable in-house — with one specialist hire.
The technical read splits the SoW cleanly: everything except the real-time media plane is work our existing Python, Next.js and DevOps engineers already do. The telephony layer is the one genuinely specialist piece — and it is also the heart of the product, so it gets a dedicated owner rather than a web team learning VoIP under deadline.
What the SoW actually asks for
Six jobs behind the “ISA proxy” analogy.
A real-time voice-AI middleware platform for BPO outbound dialing: it sits between client ViciDial/Asterisk dialers and AI voice engines.
The SoW's “ISA proxy” layer. One entry point serves every client cluster, and the first thing it must do on each inbound call is work out whose call this is — before any audio is processed or any money is spent.
- Tenant resolution — inbound SIP headers and source IP are matched against a tenant registry to bind the call to one workspace (Profile_A vs Profile_B in SoW language).
- Hard isolation per tenant — credentials, billing keys, prompt text blocks, voice definitions and webhook targets live in separate logical partitions. Nothing crosses.
- Concurrency quotas — each tenant has a channel ceiling (SoW example: 10 for Tenant A, 25 for Tenant B) plus a global software-wide cap. Enforced in Redis so the count is correct across processes.
- Unauthorised sources — connections from unknown IPs are dropped immediately with a 403-equivalent signalling error, never partially processed.
- Channel lifecycle — every accepted call becomes a lightweight non-blocking async task; on completion its state cache is purged and the slot returns to the pool.
Every answered call is classified before the expensive AI leg is bridged. The SoW asks for a verdict inside ~1.5 seconds using streaming voice-activity detection alongside live transcription.
- Answering machine / voicemail — intercept the long greeting, wait for the tone, play the voicemail drop, hang up fast.
- Business IVR — detected and navigated out-of-band by issuing DTMF tones, rather than talking to a menu.
- Dead air / silence — fire a check-in hook, then execute a clean drop if nothing comes back.
- Live human confirmed — the only outcome that proceeds to the AI dialogue engine.
A bidirectional streaming pipeline: raw trunk audio becomes text, text feeds the LLM with the tenant's script and rules injected, and the reply is streamed back down the trunk as speech — continuously, not in turns.
- Barge-in — when incoming audio energy crosses the human-speech threshold, the outbound stream stops immediately. Without this the bot talks over people and the call is lost.
- Silence handling — a gentle “Hello, are you there?” after 2.5s of silence; terminate if a further ~3s passes with nothing.
- Gated qualification — conversation checkpoints are tracked against a structured schema array, with compliance flags evaluated in real time to drive branching.
- Per-tenant behaviour — script, voice clone and LLM temperament all come from the resolved tenant profile, injected at session start.
The commercial payload of the whole platform: a qualified conversation has to become a human-to-human call, and an unqualified one has to be recorded correctly in the client's dialer.
- Qualified branch — hit the ViciDial conference API to bridge the call into the human closer queue, then drop the bot leg out of the stream so the prospect is left talking to a person.
- Unqualified branch — call ViciDial's external status API to tag the lead
NIorDNC, then disconnect the audio loop. - Session reset — purge the thread's state cache so no text memory leaks into the next call on that channel, then free the slot for the next dialer trigger.
{{variable}} injection. RBAC support staff edit only campaign variables. Immutable audit: who, when, IP, before/after.A deliberate separation between core codebase and day-to-day campaign changes, so a support agent adjusting today's promo can never break the system or see its internals.
- Guardrail by design — master prompts are locked, immutable templates containing placeholders such as
{{daily_campaign_update}}and{{dynamic_lead_criteria}}. Prompt internals, webhooks and billing structures are hidden from non-admin accounts. - Campaign requirements box — a plain-text field mapped straight to variable injection: support staff type daily script rules, active promos or targeted offers in plain English, and it propagates to active bots immediately.
- Voice & accent controls — dropdowns to swap voice clones, accent styles or synthetic engines (ElevenLabs, Cartesia) per tenant workspace.
- Immutable activity log — every admin action records a high-precision UTC timestamp, the user's username / login email / origin IP, and the exact target campaign, variable modified and before & after values.
A second agent that monitors human agent conversations live and scores compliance — explicitly without needing root access to the client's call-recording servers.
blind_monitor — the Non-Agent API silently joins an active agent session. Needs only a basic API user, not Level 9 admin. Our primary path.- Stereo transcription — streaming STT with the agent channel separated from the customer channel.
- Rule evaluation — an LLM matches the live transcript against the target script: mandatory compliance lines, buyer requirements, qualification markers.
- Compliance scoring — did age verification, medical-status checks and disclaimers actually happen? Met or missed, per requirement.
- Ticket out — within 60 seconds of hangup, a compliance ticket with metrics, verified script markers and the full transcript is webhooked to the client CRM.
Key decisions · made, with rationale
Four calls that set the whole shape.
Everything downstream — team, timeline and cost — follows from these. Each one was chosen to de-risk the launch without closing off the better long-run economics.
| Decision | Choice | Why | Detail |
|---|---|---|---|
| D1AI engine strategy | Hybrid via adapterChosen | Launch on a managed engine (Retell or Vapi, decided by bake-off) behind our own interface. Fastest to pilot; keeps a later custom pipeline as a swap, not a rewrite. | + The SoW itself leaves this open — it names “Retell AI, Vapi, or raw custom pipelines”. Those are wildly different projects, so this was the first fork to close. ChosenManaged engine now, behind an adapter interface. The engine is a swappable dependency, not a foundation.
Rejected AManaged engine forever — smallest build, but per-minute cost compounds indefinitely and we stay exposed to their pricing and concurrency limits at 500 channels.
Rejected BCustom STT/LLM/TTS from day one — best unit economics, but adds ~3–5 months of the hardest real-time audio engineering and is the highest-risk path for a team without VoIP experience.
What it costs us ◇ Roughly $0.08–0.14/min all-in while we are on a managed engine — treated as a pass-through-with-margin line, never absorbed.
|
| D2Media entry architecture | Own SIP media edgeShapes everything | We present to the client as a plain SIP destination. Works with any ViciDial version, no client-side surgery, tenant identity lands on SIP headers, and triage stays on our metal. | + The single most consequential technical decision in the plan — it determines onboarding friction, security posture, and the engine bill. Option A
not chosenDirect AudioSocket. The SoW's literal suggestion: each client streams raw PCM from their Asterisk dialplan to our TCP endpoint. Least infrastructure for us — but it requires Asterisk 18+ (many ViciDial installs are older), needs invasive dialplan changes on every client box, sends unencrypted audio over the public internet, and turns each onboarding into a support project. Option B
chosenOur own SIP media edge (Kamailio/FreeSWITCH + RTPengine). ViciDial routes answered calls to us like it routes to any carrier. Works with any version, zero client-side surgery beyond one route entry, tenant identification lands naturally on SIP headers and source IP exactly as the SoW describes, and TLS/SRTP becomes possible. Option C
laterB now, A later — direct AudioSocket returns as an opt-in low-latency route for clients already on modern Asterisk. A phase-3 menu item, not a launch dependency. Commercial, not just technical ◇ Because triage runs on our edge before the engine leg is bridged, we filter machines and IVRs at near-zero cost. That single choice roughly halves the engine bill — the difference between the managed-engine path being viable or not at 500 channels.
|
| D3Launch scale | Pilot first: 25–50 chThen 100, then 500 | Prove AMD accuracy and transfer flows on real traffic before capacity hardening. Earliest revenue, lowest risk. | + The SoW names 100 concurrent bots as “initial scale” and 500 as “target bulk scale”. We deliberately go live below both.
Why not 100 at go-live ◇ It adds 2–3 weeks of load-testing and capacity hardening to the critical path before a single real call is handled — spending hardening effort on numbers we have not yet validated behaviour at. All three tiers are costed regardless.
|
| D4Operating model | Build + operateRecurring revenue | We run it in production and onboard tenants. Adds an ops staffing tail and an on-call rotation that must be agreed before committing. | + Chosen over build-and-hand-over. It is the better commercial position, but it is a standing obligation, not a project with an end date.
Say it out loud ◇ Agree the on-call rotation with the team before committing to operate. A managed service sold without an answer to nights and weekends becomes an attrition problem in month 6.
|
Architecture · the call path
Filter on our metal. Pay only for humans.
- their trunks dial, their carrier cost
- onboarding = one route entry
Each client keeps running their own ViciDial/Asterisk cluster exactly as they do today. Nothing about their dialing, lead lists or carrier arrangements changes.
- They dial, they pay the carrier. Trunk costs stay entirely client-side — our platform only ever receives calls that have already been answered.
- Onboarding is one route entry — they point answered calls at our SIP address the way they would point at any carrier or agent group. No dialplan rewriting, nothing installed on their boxes.
- Version-agnostic. Because we speak plain SIP rather than requiring Asterisk 18+ AudioSocket, older ViciDial installs work unchanged — the deciding factor in decision D2.
- They are also the transfer destination — qualified calls come back to their human closer queues through the conference API.
+ RTP audio
- bot leg drops, a person takes over
bridge
- resolve tenant — headers / IP
- allowlist → 403 unknown
- TLS / SRTP
The SoW's “ISA-style policy proxy”, made concrete. We run a real telephony stack so that to every client we are simply a SIP destination — and so that all policy is enforced on hardware we control.
- Tenant resolution — inbound SIP headers and source IP are matched against the tenant registry, binding the call to one workspace and its script, voice clone and LLM temperament.
- Admission control — unknown IPs are refused immediately with a 403-equivalent signalling error. Nothing unauthenticated gets as far as audio processing.
- Media handling — RTPengine relays the audio and hands raw 16 kHz linear PCM to the session core over native sockets, exactly as the SoW mandates (no browser-emulation layer anywhere).
- Encryption — TLS for signalling and SRTP for media wherever the client's side supports it; the direct-AudioSocket alternative could not offer this.
- per-tenant concurrency quotas
- VAD · AMD · IVR-DTMF — ~1.5s
- qualification gate vs schema
The heart of what we build and own. Every accepted call becomes a lightweight non-blocking async task here, and three distinct jobs happen in sequence.
- Session reset — on completion the thread's state cache is purged so no text memory leaks into the next call, and the channel slot returns to the pool.
- Stack — Python 3.11+ with FastAPI/asyncio, per the SoW's mandated runtime.
only
50–70%
Not a failure path — the business model. Most of what an outbound dialer connects to is not a person, and every one of those calls is filtered here, on infrastructure that costs us almost nothing to run.
- Answering machines — intercept the greeting, wait for the tone, play the voicemail drop, hang up.
- Business IVRs — navigated out-of-band with DTMF tones rather than conversation.
- Dead air — a check-in hook fires, then a clean drop when nothing returns.
- All three terminate before the engine leg is bridged, so they consume zero paid engine minutes.
- STT → LLM → TTS, barge-in
- locked prompt template injected
Only confirmed humans arrive here, and this is the first point at which we spend money per minute. The adapter is a thin interface of our own design; the provider sits behind it.
- What the provider does — the streaming STT → LLM → TTS loop, barge-in when the human starts speaking, and the sub-second latency budget. Retell or Vapi, chosen by bake-off in phase 0.
- What we do — inject the tenant's locked prompt template with its campaign variables resolved, hold the qualification schema, and own the call's lifecycle either side of the conversation.
- Silence behaviour — a nudge at 2.5s, terminate if a further ~3s passes, per the SoW.
- qualified → warm transfer
- else → tag NI / DNC
- purge state, recycle channel
Where the platform delivers or destroys its value. A qualified conversation must become a human-to-human call, and an unqualified one must land correctly in the client's dialer.
NI or DNC, then disconnect the audio loop. The disposition is what the client's reporting and compliance depend on.- The same integration layer later carries
blind_monitorfor the QA bot's live audio tap in phase 3.
Amendment A's control plane: the surface through which support staff change campaigns without touching the system. Next.js front end against a FastAPI service that shares the core's data models.
- Campaign requirements box — plain English in, variable injection out. Daily script rules, active promos and targeted offers propagate to active bots immediately.
- Voice & accent dropdowns — swap voice clones, accents or synthetic engines (ElevenLabs, Cartesia) per tenant workspace.
- RBAC — prompt internals, API webhooks and billing structures are invisible to non-admin accounts. Support staff can only reach the variables.
- Audit log — append-only and hash-chained, capturing UTC timestamp, username, login email, origin IP, target campaign, variable changed and before/after values.
Amendment B, and a separate product surface: it monitors human agent calls for compliance, not our own bot calls. Delivered in phase 3.
- Audio tap — ViciDial's
blind_monitorNon-Agent API is our primary route because it needs only a basic API user, not Level 9 admin. SIPREC forking is offered to clients who actually run an SBC; a ghost-agent WebRTC listener is the fallback. - Stereo transcription — streaming STT with the agent channel separated from the customer channel.
- Rule evaluation & scoring — an LLM checks the live transcript for mandatory compliance lines, buyer requirements and qualification markers, then scores whether age verification, medical-status checks and disclaimers were met or missed.
- Ticket out — compliance metrics, verified script markers and the full transcript webhooked to the client CRM within 60 seconds of hangup.
The foundation, and a point of compliance with the SoW: every mandated technology is used as specified, with nothing substituted.
Technical architecture
Components, transports & data stores.
TLS / SRTP
webhooks
Build vs buy
Buy the commodity. Build the product.
Buy
Where someone else's core business beats ours
- ⇩STT / LLM / TTS orchestration — Retell or Vapi, behind our adapter. Sub-second voice loops are their entire business; rebuilding now is months of risk for zero pilot value.
- ⇩Voice clones & accents — ElevenLabs / Cartesia, per the SoW. A per-tenant config choice exposed in the portal.
Build
The differentiation and the margin
- ◆SIP edge, tenant routing, quotas, triage, qualification gating, dispositions & warm transfer — this is the product. Nobody sells it as a unit.
- ◆Support portal, RBAC, immutable audit, QA bot, CRM webhooks — standard web/API engineering; our existing team's home turf.
Team plan · who builds it
Peak seven. One new face.
| Role | Who | Allocation | Owns |
|---|---|---|---|
| Tech lead / architect | Senior Python · existing | Full-time | Architecture, engine adapter, code reviews, client technical interface |
| Backend engineer | Senior Python #2 · existing | Full-time | Session pool, Redis quota enforcement, qualification gating, call lifecycle |
| Integrations engineer | Mid Python · existing | Full-time | ViciDial APIs — dispositions, conference transfer, blind_monitor — webhooks; QA bot in phase 3 |
| Telephony / VoIP engineer | Senior VoIPnew hire | Full-time | SIP media edge, AMD/IVR triage tuning, SIP bridging, SIPREC later |
| Portal engineer | Next.js dev · existing | From ~week 6 | Support portal, RBAC UI, campaign variable editor, audit views |
| DevOps | Existing team | ~0.5 FTE avg | IaC, CI/CD, Compose→K8s, observability, load-test infrastructure |
| QA / SDET | Existing QA | From phase 1 | SIPp call simulator, regression suite, compliance scenarios |
| PM / EM | Existing | 0.25–0.5 | Client, scope, hiring |
| Peak 7 · average ~6 · one new hire | 40–45 pm | Then 1.5–2.5 FTE to operate it as a managed service after GA | |
The hire is the critical path
Start recruiting day one
- ◆If it runs past ~4 weeks, bridge with a freelance Asterisk/FreeSWITCH contractor — ViciDial specialists are a healthy niche market.
- ◆Decide at month 4 whether a second telephony-leaning engineer is needed for on-call depth. Not now.
Two honest caveats
Say these out loud before committing
- ⚠Managed service means on-call. Three people must be able to answer a 2 a.m. “calls aren't transferring” page: telephony, a senior Python, DevOps.
- ⚠The .NET developers stay off this project. The SoW mandates Python/Node; forcing .NET in adds a stack for no benefit.
Phased build plan
Pilot at month 3. GA at month 5.
blind_monitor + STTCost model · monthly run rate
Infrastructure is cheap. Minutes are not.
| Cost line | Pilot · 50 ch | GA · 100 ch | Scale · 500 ch |
|---|---|---|---|
| Compute — SIP edge + app core | $300 | $600–800 | $2,200 |
| PostgreSQL + Redis (multi-AZ from GA) | $180 | $645 | $1,290 |
| K8s, LB, NAT, S3, monitoring, backups | $200 | $450 | $800–1,000 |
| Staging + dev environments | $300 | $350 | $400 |
| Egress (audio ≈ 64 kbps/leg) | $50 | $100 | $450 |
| AWS subtotal on-demand | $1.0–1.4k | $2.1–2.5k | $4.7–6k |
| Paid engine-minutes / month after triage filtering | ~155k | ~310k | ~1.5M |
| AI engine cost $0.08–0.14/min; negotiated at scale | $12–22k | $25–43k | $105–185k |
Risk register · the ones that bite
What we watch, and how we cover it.
Delivery risk
Schedule and capability
- ⚠Telephony hire slips past 4 weeks → recruit from day 1; contractor bridge ready.
- ⚠Client ViciDial version quirks → our own SIP edge minimises client-side dependency; lab + staging in phase 0.
- ⚠Voice-to-voice latency over ~1s → measured in the phase-0 spike, before commitment; region colocation.
- ⚠On-call burnout on a small team → three trained responders, alert hygiene, 2nd hire reviewed at GA.
Commercial & compliance risk
Margin, trust and law
- ⚠AMD accuracy below the business bar → wasted engine spend; measured from pilot day 1, target agreed with client.
- ⚠Engine concurrency caps or price moves → enterprise agreement before scale; the adapter keeps alternatives credible.
- ⚠Cross-tenant data leakage → tenant-scoped keys, isolation tests in CI, security review, pilot exit criterion.
- ⚠TCPA/DNC, recording consent, medical data → client owns campaign compliance; we supply disposition mechanics, audit trail, retention controls.
Next steps
Six answers we need from the client.
The plan holds without these — but each one moves a number. Volumes move the engine bill most.