my whatsapp flight tracker bot drove 5 leads and made me $7.25 in commissions
thats from 3k+ numbers and 10k total messages
considering the volume its pretty low and not worth the effort.
with muse taking over agentic commerce, bots like this have no future
still, most of yall asked how i built it and heres the stack:
→ llm: gemini 3 flash via @WiroAI
→ flight fares: aviasales via travelpayouts.com/?marker=531518
→ whatsapp number + cloud api: zernio.link/onur-ozcan @zernio
→ backend: railway.com/?referralCode=…
prompt:
Build a production WhatsApp AI assistant. Python 3.13, async throughout,
deployed on Railway. WhatsApp is the only channel.
WHAT IT DOES
[What the assistant helps people with, and what users can subscribe to alerts for]
[The upstream data API it calls, with docs URL and auth]
[Language it speaks to users in]
STACK
httpx, aiohttp, asyncpg (raw SQL, no ORM), APScheduler, python-dotenv. Nothing
else. One process runs the webhook server and the scheduler on one event loop.
Config from env vars only. Procfile + requirements.txt.
Providers: Zernio (docs.zernio.com) over Meta Cloud API for WhatsApp; Gemini 3
Flash for the LLM — call it directly with JSON mode, a queue-and-callback
transport adds ~8s per turn; Railway + Postgres for hosting.
LAYOUT
app/main.py, config.py
app/handlers/whatsapp.py webhook event -> engine -> send (thin adapter)
app/services/ engine.py (channel-neutral core), llm.py, upstream.py,
reference.py, database.py, scheduler.py, delivery.py,
whatsapp_api.py, webhook.py (aiohttp routes, HMAC, /healthz)
app/utils/ formatters.py, wa_format.py
scripts/setup.py verify creds, register webhook, print account ids
CORE DESIGN
engine.py takes (user_id, text, state) and returns a Reply of text + buttons. It
knows nothing about WhatsApp; the handler is the only file touching payloads.
The LLM never produces data values. One call per turn returns
{"action": <fixed set>, "message": <prose>, ...params}. Python dispatches on
action, calls the upstream API, and formats every number itself.
FLOW
Verify HMAC on the raw body -> dedupe by event id -> return 200 immediately ->
process in a background task under a per-sender lock -> typing indicator ->
engine -> convert to WhatsApp markup -> send. Log one line per turn: user,
action, params, model time, API time.
DATABASE (schema created idempotently at startup, guarded ALTER TABLE)
users(user_id, profile, message_count)
subscriptions(id, user_id, domain fields, conversation_id, last_value,
last_checked_at, created_at) -- conversation_id is required for
proactive sends
WHATSAPP GOTCHAS — implement all
- Ack in under 5s; an LLM call is slower, so always background the work.
- At-least-once delivery: dedupe on event id.
- A 200 from the provider is not delivery. Subscribe to the failure webhook and
record Meta's error code; never count accepted as delivered.
- The 24-hour window is the central constraint: free-form replies only within
24h of the user's last message. Everything proactive needs an approved
utility template. Submit templates and get the business display name approved
BEFORE launch — review takes a day, and a rejected name blocks sending.
- Button taps may carry their payload at the event root, not on the message.
Read both, then fall back to matching the tapped title.
- No HTML: convert to *bold*/_italic_/bare URLs, markers can't span newlines,
cap at 4096 chars on a line boundary, 3 buttons of 20 chars max.
RELIABILITY — all of these are real production bugs
- Retry every upstream call once; log status and body on failure. A swallowed
error looks exactly like "no results" and reaches the user as a silent gap.
- Validate LLM-produced identifiers against the provider's reference data
loaded at startup; models emit codes for things that were retired.
- Check LLM-produced dates: roll past dates forward, drop inverted ranges.
- Trim history on write, not just when building the prompt.
- Rate limit per user (~30 turns/hour) or one person runs up the model bill.
- Persist state in Postgres, not process memory — deploys wipe memory.
- /healthz wired to the host health check, plus Sentry on day one; hosted log
tails are too short to debug anything after the fact.
DELIVER
Code in the layout above, README covering every env var and provider setup, and
scripts/setup.py that verifies credentials and registers the webhook.
@oozn · Agents & automation