Velya
An AI receptionist that works a business's Instagram and WhatsApp inbox: answering from its knowledge base, capturing leads, and booking appointments.
- Role
- Lead Full-Stack Engineer
- Dates
- Sep 2025 – Jun 2026
Read as steps
- 01Live conversations are assigned to a variant.
- 02Each variant runs its own prompt or model configuration.
- 03A chi-square sample-ratio-mismatch check gates the result before anyone reads it.
- 04An LLM-as-judge supervisor scores conversation quality alongside the statistical test.
The problem
Clinics and service businesses now get most of their inbound demand through Instagram DMs and WhatsApp rather than phone or email. Those messages arrive at all hours and mostly repeat the same handful of questions — price, availability, location, whether the clinic treats a particular condition — and every one left unanswered overnight is a booking that goes elsewhere. Paying someone to sit on the inbox is expensive and still does not cover evenings and weekends. Velya puts an agent on that inbox which answers from the business's own material, books into its calendar, and hands the conversation to staff when it should.
What it is
A multi-tenant SaaS product. A business connects its Instagram and WhatsApp accounts and supplies the material the agent should answer from; the agent then handles the inbox — answering questions, qualifying leads, capturing contact details, booking appointments, and escalating to staff. Staff work in a 15-module admin console and a real-time client application built on Convex with 83 serverless functions. Billing is metered per organisation, with custom org-scoped plans, overages and credit grants, collected through Stripe or Chargily. Behind all of it sits an experimentation and evaluation layer that treats every prompt and model change as something to be measured before rollout.
My role
Lead and primary engineer, September 2025 to June 2026, responsible for roughly 80% of the build by my own estimate. That share includes the hexagonal TypeScript backend end to end, the agent and its tool-calling layer, the A/B and evaluation stack, metered billing, the admin console, and major parts — not all — of the Convex client application.
Who uses it
Clinics and service businesses in markets where Instagram and WhatsApp are the primary customer channel. Their staff use the console and client app to review conversations, pick up escalations and manage bookings. Their customers simply message the business account and get an answer — including when what they send is a photo, a reel or a story rather than text.
Features
Instagram and WhatsApp agent
One agent works both inboxes as the business's front desk. It replies in the conversation the customer already started, rather than redirecting them to a form or a separate chat widget.
Knowledge-base answers
The agent answers from the material the business supplies about itself — services, prices, hours, policies — so its answers are the business's answers rather than general knowledge.
Appointment booking
Booking is a tool the agent calls, not text it writes. It gathers what a booking needs during the conversation and creates the appointment itself.
Grounded lead capture
The agent records contact and qualification details from the conversation. Each captured field is checked against the transcript before it is stored, so a phone number is saved only if the customer actually sent it.
Image, reel and story understanding
Customers frequently send a photo or reply to a story. The agent has vision understanding of those images, reels and stories, so it can respond to what was sent rather than asking the customer to type it out.
Escalation to staff
Conversations the agent should not finish are handed to a person, and staff pick them up in the console with the conversation intact.
Admin console
A 15-module React and Ant Design console covering the operational surface of the product for staff and administrators.
Real-time client app
A client-facing application built on Convex with 83 serverless functions, where conversation and booking state updates live rather than on refresh.
Metered billing
Usage-metered subscriptions with organisation-scoped custom plans, overage handling and credit grants, charged through either Stripe or Chargily depending on the customer.
Architecture
- Clean, hexagonal Node.js backend
- 25 domain entities, 31 services and 27 repositories in TypeScript. Domain logic is isolated from transport and persistence, so channels and providers are replaceable at the edge.
- Explicit composition and validation
- Inversify for dependency injection, so wiring is declared rather than implicit, and Zod schemas validating at every entry point — external message payloads, API calls and provider webhooks are parsed into typed values before any domain code sees them.
- Data layer
- Supabase-hosted PostgreSQL for durable state with Redis alongside it for the fast paths and coordination that a live inbox requires.
- Agent tool-calling layer
- OpenRouter for model access with the Vercel AI SDK driving tool-calling, so booking, lead capture and knowledge-base lookup are typed tools the model invokes, and the model provider can be changed without rewriting the agent.
- Access control
- JWT authentication with role-based access control for user traffic, and API-key middleware for service-to-service calls, keeping the two kinds of caller on separate authentication paths.
- Experimentation and judging stack
- An A/B engine backed by a frequentist statistics library written from scratch, an LLM-as-judge supervisor scoring conversation quality, and a prompt-versioning and regression-testing pipeline that every prompt change passes before rollout.
- Client surfaces
- A 15-module React and Ant Design admin console, plus major parts of the real-time Convex client application and its 83 serverless functions.
Hard problems
The parts that took the most thinking, and what I actually did about them.
There is no obvious way to tell whether a prompt change made the agent better. Conversation quality is subjective, live traffic is noisy, and a change that looks clearly better across ten transcripts can be worse in aggregate. Shipping prompt changes on impression is how agent quality drifts without anyone noticing.
What I did
Prompt and model changes go through an A/B experimentation engine backed by a frequentist statistics library written from scratch: two-proportion z-test, Welch's t-test, Mann–Whitney U, Wilson confidence intervals, chi-square sample-ratio-mismatch detection and power analysis. The SRM check matters as much as the test itself — if traffic did not actually split the way the experiment assumed, the result is not read at all. An LLM-as-judge supervisor scores conversation quality alongside it, and a prompt-versioning and regression pipeline re-runs past cases so a change cannot silently regress what already worked.
An agent that records a phone number the customer never gave produces a lead that cannot be contacted and a booking that cannot be honoured. A plausible invented value is worse than a blank field, because nothing downstream can tell the two apart.
What I did
Captured data is validated against the conversation transcript before it is persisted. A phone number is stored only if the customer actually provided it in the conversation — a value the model inferred, completed or assumed does not pass. The failure mode becomes a missing field, which staff can see and chase, instead of a wrong one that looks correct.
Messaging is concurrent in a way request-response systems are not. A customer sends three messages in quick succession while the agent is still generating a reply to the first. Replying to a superseded message makes the agent look broken, and in a booking flow it can act on instructions the customer has already changed.
What I did
Race-condition guards around message handling, so the agent does not answer a message that has since been overtaken. This is ordinary concurrency control, but it is the kind of thing that only shows up under real inbox traffic and is easy to leave out of a demo.
Every business wants a different commercial arrangement — different included volume, its own overage rate, sometimes a credit grant — while the two payment providers in play serve different markets: Stripe internationally, Chargily locally.
What I did
Plans are modelled as organisation-scoped custom plans with overages and credit grants, and usage is metered independently of which provider ultimately collects the money. A commercial arrangement is described once in the billing domain rather than reproduced in each provider's own vocabulary.
Stack
- TypeScript
- Node.js
- Inversify
- Zod
- Supabase
- PostgreSQL
- Redis
- OpenRouter
- Vercel AI SDK
- Convex
- React
- Ant Design
- Stripe
- Chargily
- Meta / Instagram OAuth
This is commercial work, so there is no public repository or demo to link. I am happy to walk through the architecture, the evaluation harness, or any decision on this page in a call.