The problem
A small Indian tour agency sells fixed-date group trips: a set itinerary, a price per person and a limited number of seats per departure. Most run on WhatsApp, phone calls and a spreadsheet, so two people can pay for the same last seat, prices are worked out by hand, and nobody can see who changed a booking. Tripsmith is that agency’s own site, built so every seat, rupee and change is accounted for.
What I built
A Next.js site with 14 packages and 51 departures; package and destination pages are generated ahead of time and refresh on demand when the owner edits them. A FastAPI + Postgres API behind it. Customers book with live seats, add-ons, deals, early-bird prices and coupons; pay in full or a 25% deposit through Razorpay in test mode; get a PDF voucher and GST invoice; and sign in with an emailed 6-digit code to manage their trips. Customers can also join a waitlist on a sold-out date, change their date, save and compare trips, fill in traveller details and open a trip pack a week before they leave. The owner runs bookings, refunds, enquiries, packages, coupons, reviews and counter bookings over Razorpay Payment Links from an admin desk on the same API, with a departure calendar and reports that reconcile with the bookings CSV. On every public page an AI trip planner plans the trip in a chat: it searches live packages, checks dates and seats, opens the booking sheet pre-filled or hands the chat to a person, and the owner reads every conversation in a log.
Key decisions
The server owns the price
Every quote is built on the server in whole paise: deal, then early-bird, then coupon, with add-ons added last. The browser never sends a price; the owner’s counter screen sends the total it expects and gets 409 price_changed if it moved.
Seats are held, not hoped for
Pressing Pay creates a 10-minute hold. The server locks the customer’s email or phone, then the departure row (FOR UPDATE). A test fires two orders for the last seat at once: exactly one wins, the other gets 409 sold_out.
All payments go through one capture function
The Checkout callback, the sync when Checkout closes, the webhook and payment links all lead into it. Signatures are checked with HMAC and a unique Razorpay payment id turns replays into no-ops. A payment that lands after the seats are gone is cancelled and refunded automatically through the Razorpay refund API.
History can’t be rewritten
Every booking change is written to booking_events, and a Postgres trigger refuses any UPDATE or DELETE on it. The owner sees it as a timeline, the customer as “Activity”.
The trip planner can only quote the database
A tool-calling agent on Gemini’s free tier, with our own loop instead of a framework. Every card and price comes from a tool result, and a guardrail checks the reply while it streams: an invented package or ₹ amount is replaced before it reaches the browser. Phone numbers and emails are removed before the model sees them, the site-wide cap lives in Postgres so it holds if the rate limiter fails, and when the free turns run out a no-AI finder takes over. 23 scripted real-model conversations pass, prompt injections included.
Versions
- v1 Agency website Shipped
Catalog, search, package pages, enquiries + WhatsApp, itinerary PDF, owner admin.
- v2 Booking engine Shipped
Razorpay checkout, webhooks, accounts, deals, coupons, reviews, owner bookings desk.
- v2.5 Strengthen Shipped
Deposits, add-ons, early-bird, a waitlist, date change, refunds + GST documents, traveller details, a trip pack, saved trips + compare, counter booking, a departure calendar, reports and automatic emails.
- v2.6 Hardening Shipped
A four-track audit closed: money edge cases, demo-owner limits, CI gates, indexes, and every business rule served from one API.
- v3 AI trip planner Shipped
Chat that searches the catalog, checks live seats, opens the booking or hands off to a person; streaming guardrail, owner log, no-AI quick finder.
Outcome
Complete and live at tripsmith.virajdomadia.com, taking bookings in Razorpay test mode, with the AI trip planner on every public page at ₹0 a chat. Over 1,150 API tests and over 740 web tests, with a forced-race test on every lock path, plus 23 real-model evals run by hand (23/23). CI runs three jobs: web, api, and an OpenAPI contract check that fails if the generated types drift. The booking page scores 88 on PageSpeed mobile with the booking sheet open.
Updated











