Case study · Agentic AI

Tourista

Most "AI trip planners" invent itineraries against no inventory at all. Tourista gives the model real, bookable data and lets it do the searching — one plain-English sentence in, one costed trip out, streamed as it assembles.

Type
End-to-end product — architecture, backend, frontend, AI agent
Status
Working MVP with a one-command live demo
Stack
React 19 · TypeScript · Node/Express · MongoDB · Claude · Duffel · Stripe · MCP
Source
github.com/AhmedAliQadir/Tourista

Note on the name: this is the Orbixio product. It is not the 2024 tour booking platform I built as my honours project at Glasgow Caledonian University, which shares the name and nothing else.

Context

Planning a trip means opening ten tabs — a flights site, two hotel sites, a car-hire comparator, a tours marketplace — and then reconciling dates, budgets and logistics in your head. The obvious use for a language model here is the reconciliation. The non-obvious part is that almost nobody does it properly, because giving a model real inventory is much harder than letting it improvise.

The generic AI trip planner produces a beautiful itinerary describing flights that do not exist at prices that were never real. It demos well and it is worthless. Tourista exists to be the other thing.

The problem

Three problems, and only the first one is about AI:

What I built

A three-package system with real boundaries

frontend/     React 19 + Vite + TypeScript + Tailwind + Framer Motion (SPA)
backend/      Node + Express + TypeScript + MongoDB/Mongoose (REST + Socket.io)
duffel-mcp/   Model Context Protocol server exposing the travel tool layer

Five typed tools, one agent loop

A manual Claude tool-use loop drives five typed tools: resolve_place, search_flights, search_stays, search_cars and search_tourista_tours. The first four hit the Duffel API; the fifth queries Tourista's own curated products in MongoDB, so the platform's own inventory competes on equal footing with third-party supply. The loop is bounded — a tool-calling agent without a call ceiling is an unbounded bill.

Streaming, so the wait becomes the interface

Results stream to the browser as NDJSON over a single response: text deltas plus live tool-progress events. The itinerary types out as it is written, and each tool call surfaces in the UI as it happens — "Searching flights MAN→BCN". The thirty-to-sixty-second blank spinner became a visible process. Nothing about the underlying latency changed; everything about the experience of it did.

One tool layer, two consumers

The Duffel integration is written once and consumed twice — by the web backend, and by a standalone MCP server that exposes the same tools to Claude Desktop. The same travel tools work inside the product and inside any MCP client, with no duplicated logic and no drift between them. This is the piece of the architecture I am most confident about: it treats tool logic and transport as separate concerns, which is exactly what MCP is for.

The rest of a real product

JWT auth, a tours catalogue, bookings, reviews, Stripe checkout via PaymentIntents, Cloudinary media, Socket.io, Helmet, CORS and rate limiting, Winston logging and central error handling. The AI planner is the interesting part; it is sitting on a product that works without it.

A demo anyone can run

npm run demo boots an ephemeral in-memory MongoDB, starts the real API, and seeds users and eight rich tours. No Atlas account, no Docker, no cloud keys. A reviewer with Node installed sees the whole stack running in one command.

A motion system rather than scattered animation

A shared motion module defines easings and variants, and nine reusable animated components — Aurora, Marquee, TextReveal, MagneticButton, Spotlight, BentoGrid, CountUp, Skeleton, ScrollProgress — keep every page consistent. All of it respects prefers-reduced-motion.

Decisions I would defend

A manual tool-use loop instead of an agent framework

Frameworks are excellent until the day the behaviour you need is one the abstraction does not expose — and with a streaming, bounded, tool-progress-emitting loop, that day arrives immediately. I wrote the loop. It is a few hundred lines, I can reason about every branch of it, and the retry, ceiling and error-surfacing behaviour is mine rather than inherited.

Streaming NDJSON rather than WebSockets for the planner

The planner is one request producing one growing response. That is what a streaming HTTP response is. Socket.io is already in the stack for other features, so reaching for it here would have been free in effort and wrong in shape — a persistent bidirectional channel for a unidirectional request/response is complexity with no return.

Search-and-assemble, not merchant of record

Tourista searches and assembles; it does not sell a packaged holiday. That is a deliberate product boundary drawn around the UK ATOL and Package Travel Regulations line, decided before the booking flow was built rather than after. The affiliate-referral route to revenue exists precisely because it keeps the product on the right side of that line.

Giving the model tools rather than context

The alternative design — fetch a pile of options, stuff them into the prompt, ask for an itinerary — is simpler and fails in a specific way: the model can only work with what you guessed it would need. Tools let it ask its own follow-up questions. It costs more round trips and produces a system that holds up when the request is unusual.

The result

What I would do next

Deploy behind a domain (Vercel, Render, MongoDB Atlas); add an affiliate-link mode via Travelpayouts to monetise referrals without merchant-of-record obligations; and narrow to a defensible niche instead of competing with everyone at once.

My role and stack

Role: Solo full-stack engineer and systems designer — architecture, backend, frontend, AI agent, developer experience. Built under Orbixio Ltd, where I am Founder.

Frontend: React 19, TypeScript, Vite, Tailwind CSS, Framer Motion, Zustand, React Router, Stripe Elements.
Backend: Node.js, Express, TypeScript, MongoDB, Mongoose, JWT, Socket.io, Stripe, Cloudinary, Helmet, Winston.
AI and integrations: Anthropic Claude (tool-use and streaming), Duffel flights/stays/cars, Model Context Protocol.
Tooling: mongodb-memory-server, ESLint, tsx.