Vendwa
An autonomous WhatsApp sales agent, built and operated by Bestdid Technology Ltd in Accra.

Vendwa is an autonomous sales agent that runs a vendor's customer conversations on WhatsApp and Instagram. It is operated by BestDid Technology Ltd, our Ghana-registered entity. The product lives at vendwa.com.
This is a note about how it is built, not about what it sells.
The constraint
Two things shaped every decision.
The first is that we do not own the transport. Every message arrives through Meta's WhatsApp Cloud API, which redelivers on its own schedule, signs payloads with an app secret we have to verify, and refuses to send a free-form reply outside a 24-hour window opened by a real inbound message. During the pilot the platform's free test number would only talk to five pre-approved recipients, and outbound sends to anyone else bounced with error 131030. The agent received, reasoned, wrote a reply, stored it, and then the send failed silently at the last hop. That looked exactly like a broken agent for a week. It was an allowlist.
The second constraint is harder. A sales conversation is mostly arithmetic, and a language model is bad at arithmetic in a way that is invisible until money is involved.
The stack, and why
Node and TypeScript, Express, Prisma against Postgres, the Anthropic SDK running
a tool-use loop, and Vitest. Deployed on Railway behind api.vendwa.com.
Prisma started on SQLite because a concierge pilot with three vendors did not need anything else. We moved to Postgres before the second vendor, which was the right time: early enough that the migration was one afternoon, late enough that we had not paid for a database nobody was using.
Tests run against PGlite, the Postgres engine compiled to WebAssembly, injected into the shared client through a test-only hook. That is not a preference. This machine cannot run Docker, and the shell runs elevated so a native Postgres server refuses to boot. PGlite was the only local Postgres that worked, and each test file gets its own fresh in-memory instance.
Three decisions
Money never comes from the model. Every price, discount and total the agent
states has to be returned by a tool call in the same turn. counter_offer
handles one item. quote_total handles a multi-item cart and clamps the result
between the combined floor and the asking total. capture_order returns a
payment line the agent must repeat verbatim. The trade-off is real: every new
pricing shape needs a tool before it can exist, so bundles, delivery fees and
per-unit discounts each cost a code change rather than a prompt edit. We think
that is the correct price. A prompt rule the tools cannot satisfy is a rule the
agent will break.
Three guarantees live in code, not in the prompt. Catalogue grounding,
the floor price, and silence during a human takeover are enforced by the
handler and the tools. The model can be talked into anything. capture_order
cannot.
One webhook, many channels. When Instagram DMs and Facebook Messenger were
added, we did not add endpoints. A channel-adapter registry dispatches on the
top-level object field of the incoming payload, and conversations are keyed by
(channel, channelUserId) rather than by phone number. The agent, the tools and
the order path did not change. Instagram costs us a daily cron to refresh a
60-day token and its own app secret for signature checks, because a nested
Instagram app is signed with a different secret than the WhatsApp one. That took
a while to find.
What broke
A customer on the demo shop asked for GHS 50 off each of four smocks. The asking total was GHS 3,100, so the answer is GHS 2,900. The agent said GHS 3,000, captured the order at GHS 3,000, sent a receipt for GHS 3,000, then agreed to GHS 2,900 when the customer corrected it. Verbal deal and written receipt now disagreed.
The root cause was ours. quote_total returned only the asking total and the
floor total. A per-unit discount had no code path, so the arithmetic had
nowhere to go except the model's head. Swapping models does not fix that class
of bug. Removing the arithmetic does.
Two rules came out of it. Extend the tool before you write the prompt rule. And
once capture_order returns ok, the recorded number is the number: the agent
holds it and escalates to the owner rather than agreeing a new total, even when
the customer is right.
A related fix landed the same season. The escalate tool used to set the
conversation status to escalated, which is the same flag the handler reads to
stay silent during a human takeover. Looping the owner in therefore muted the
agent from the customer's next message. Only a real operator reply sets that
flag now.
Where it is now
Live in production on Railway, with WhatsApp serving customers and Instagram
connected through an OAuth flow at api.vendwa.com. Payments, self-serve
onboarding and an iOS companion app have shipped since. Meta App Review for the
Instagram messaging permissions was the long pole, not the code.
Built by BestDid
We built and operate Vendwa. The product, its pricing and its terms live at vendwa.com, run by BestDid Technology Ltd in Ghana.
Two more from the same shelf: the Geotally case study covers multi-engine orchestration and cost control, and the FixAm case study covers phone-verified reviews. The full list is on our work page. If you want something like this built, start at services.