When we set out to build Altitude, we had a simple thesis: the way people plan travel is fundamentally broken, and the technology to fix it finally exists. What followed was months of intense engineering, 35 production audit sessions, over 200 individual fixes, and more edge cases than we ever anticipated. This is the story of how we built an AI-native travel platform from scratch — the architecture decisions, the engineering challenges, the security hardening, and the lessons that shaped the product into what it is today.
This article is written for engineers, technical product managers, and anyone curious about what it actually takes to build AI-powered software that handles real money, real bookings, and real people's vacations. We will go deep on the technical decisions, explain what worked, what we had to rebuild, and what we would do differently if we started over.
The Problem: Travel Planning is Stuck in 2010
Think about the last time you planned a trip. You probably opened Google Flights in one tab, Booking.com in another, maybe Kayak or Skyscanner for comparison. You bounced between TripAdvisor for reviews, Google Maps for logistics, and a spreadsheet or notes app to keep track of it all. If you were traveling with a group, multiply that chaos by the number of people involved.
The average traveler visits 38 websites before booking a trip, according to Expedia's own research. That process takes days or weeks. And at the end of it, you still are not confident you found the best option — you just ran out of patience.
The numbers paint a stark picture of just how fragmented the travel planning experience has become. The global online travel market reached $475 billion in 2024, yet consumer satisfaction surveys consistently show that trip planning is rated as one of the most stressful parts of traveling — ranked alongside packing and airport security. Phocuswright research shows that 67% of travelers abandon a booking flow at least once before completing it, and the average time from initial research to final booking is 45 days for international trips.
Traditional online travel agencies (OTAs) have not solved this. They have optimized for transactions: show a grid of flights sorted by price, let the user filter, take a commission. The user still has to do all the cognitive work of comparing options, understanding tradeoffs, coordinating dates across flights and hotels, and fitting everything into a budget. There is a massive gap between "I want to go somewhere warm in April for under $2,000" and "I have a complete, bookable trip itinerary."
Group travel amplifies every friction point. When three friends want to plan a trip together, they are now coordinating across different departure cities, different budgets, different schedule constraints, and different loyalty programs — all through a group chat that devolves into an endless thread of screenshots and conflicting preferences. No existing tool handles this well. Most do not attempt it at all.
The insight that drove Altitude: travel planning is not a search problem — it is a reasoning problem. Users do not need more results. They need something that understands their constraints, preferences, and tradeoffs, and synthesizes all of that into a coherent plan.
Our Approach: AI-First, Not AI-Added
Most companies that add AI to travel bolt a chatbot onto an existing booking engine. The chatbot parses your query, maps it to API parameters, and returns the same grid of results — just with a conversational wrapper. That is cosmetic AI. It does not change the underlying experience.
We went the opposite direction. Instead of adding AI to a booking engine, we built a booking engine around AI. The core architectural decision: treat trip planning as a multi-agent collaboration problem where specialized AI agents handle distinct aspects of the planning process and an orchestrator manages the coordination between them.
Here is what that means in practice. When a user says "Plan a week in Japan for two people in cherry blossom season, budget around $4,000 total," that request does not go to a single monolithic API call. Instead, an orchestrator agent breaks it down into subtasks:
- Destination research agent — identifies optimal dates for cherry blossom viewing (late March to mid-April in Tokyo, earlier in Kyushu), suggests specific cities and regions, flags visa requirements, checks travel advisories, and considers seasonality factors like hotel pricing surges during peak periods
- Flight search agent — queries real-time availability across 500+ airlines via the Duffel API, applies a 12-dimension flight scoring algorithm that weighs price, duration, layover quality, airline reliability, departure time convenience, and baggage inclusion, then finds the best options within the budget allocation
- Accommodation agent — searches across 10,000+ hotels and properties near the areas the destination agent recommended, matches to the traveler's stated or inferred preferences (quiet location, walking distance to transit, specific amenities), and normalizes pricing across different rate structures
- Itinerary agent — takes the outputs from all other agents and assembles a day-by-day plan that is actually logistically feasible, accounting for jet lag recovery on arrival days, travel time between attractions, restaurant reservations, and activity booking windows
The orchestrator does not just dispatch these tasks in parallel and concatenate results. It manages a multi-turn conversation between agents, resolving conflicts (the best-priced flight arrives too late for the hotel check-in), enforcing budget constraints across the entire trip, and iterating until the plan is coherent. The target: a complete, bookable trip plan generated in under 30 seconds.
We model trip planning as a directed acyclic graph (DAG) of dependent tasks, not a linear pipeline. Flight selection constrains hotel dates, hotel location constrains activity planning, and budget allocation across all three is a constraint satisfaction problem. The orchestrator manages this dependency graph dynamically, re-running downstream tasks when upstream constraints change. This architecture also powers our "Waves" feature for multi-origin group coordination — where travelers departing from different cities need their individual flight plans to converge at the same destination on compatible schedules.
The Tech Stack: Why Each Piece Exists
Every technology choice in Altitude was driven by a specific requirement, not trend-chasing. Here is the complete stack and the reasoning behind each component.
Next.js 14 — Frontend and Server Framework
We needed SSR for SEO (travel content pages need to rank in search), but also rich client-side interactivity for the chat-based planning experience. Next.js 14 with the App Router gave us both. Server Components handle the static and SEO-critical content — destination pages, informational guides, and landing pages that need to be indexable. Client Components power the real-time chat interface, interactive maps, dynamic pricing displays, and the collaborative trip planning workspace.
The file-based routing system also made it straightforward to build out dozens of destination and informational pages without a CMS. The App Router's nested layout system lets us share navigation, authentication state, and trip context across deeply nested routes without prop drilling. Server Actions handle form submissions and mutations with built-in progressive enhancement, meaning the booking flow works even when JavaScript is still loading.
We considered a React SPA with a separate Express or Fastify backend. The reason we chose Next.js as a unified framework: travel platforms live and die by SEO. Destination pages, "best flights to X" content, and comparison pages need to be server-rendered and crawlable. With a separate SPA, we would have needed a separate SSR layer or a static site generator alongside our app — adding operational complexity for no functional benefit. Next.js lets the same codebase serve both SEO-critical static content and the highly interactive booking experience.
Prisma + PostgreSQL — Data Layer
Travel data is inherently relational. A trip has passengers, each passenger has preferences and travel documents, a trip has flight segments, each segment has a booking reference, bookings connect to payments, payments connect to refunds, refunds have different states depending on airline policy. PostgreSQL handles this naturally with strong referential integrity, JSONB columns for flexible provider-specific metadata, and excellent query planning for the complex joins our reporting and analytics require.
Prisma gives us type-safe database access across the entire codebase — when we change a schema, the TypeScript compiler catches every query that needs updating. For a codebase with 140+ files, that is not a luxury — it is a necessity. We also rely heavily on Prisma's interactive transactions for operations that must be atomic, such as creating a booking, recording a payment, and updating trip status in a single database round-trip. Our schema includes composite indexes optimized for the queries we run most frequently: trip lookups by user, booking searches by status, and audit log filtering by action type and date range.
Redis — Caching, Rate Limiting, and Distributed State
Redis does quadruple duty in Altitude. First, it caches flight search results with a sophisticated TTL strategy (detailed below). Second, it powers our rate limiting layer — both for protecting our own APIs and for managing our consumption of upstream provider APIs that charge per-request. Third, it stores ephemeral session data, LLM conversation context, and circuit breaker state. Fourth, it serves as the coordination layer for distributed locks on scheduled tasks, ensuring that cron jobs like stale-booking cleanup and price-drop detection run exactly once even in a multi-instance deployment.
We use Redis Lua scripts extensively for atomic operations where race conditions would otherwise be a problem. Budget tracking during trip planning, rate limit enforcement, idempotency key deduplication for payments, and cache-or-fetch patterns all use Lua to guarantee atomicity without distributed locking overhead. We migrated from KEYS commands to SCAN early in production hardening to avoid blocking the Redis event loop during key enumeration.
Duffel API — Flight Booking Infrastructure
This was one of our most consequential decisions. Traditional travel platforms connect to a Global Distribution System (GDS) like Amadeus, Sabre, or Travelport. GDS integrations are notoriously complex — they use legacy protocols (Amadeus still relies on EDIFACT and cryptic command formats), require certification processes that take months, and the APIs feel like they were designed in the 1990s (because many of them were). Getting from "search" to "actually issue a ticket" through a traditional GDS requires IATA accreditation, commercial agreements, and often a dedicated integration team.
Duffel provides a modern REST API over the same underlying airline inventory, with real-time pricing, actual ticketing capability, and a developer experience that does not require a dedicated integration team. We went from first API call to issuing real tickets in under a month. The tradeoff is slightly less airline coverage than a direct multi-GDS connection, but the engineering velocity gain was enormous — and Duffel's coverage of 500+ airlines includes every major carrier and most low-cost carriers.
We evaluated Amadeus Self-Service, Sabre Dev Studio, and Duffel extensively. Amadeus's self-service tier had decent search APIs, but the path to actual ticketing (not just search results) required a commercial agreement and IATA accreditation. Sabre's developer program had similar gatekeeping. Duffel was the only provider where we could go from sandbox to production ticketing with a single integration, a single agreement, and a REST API that returned JSON instead of XML-encoded EDIFACT. For a startup building its first product, that velocity matters more than marginal airline coverage differences. See our detailed comparison for more on how this decision shaped the product.
OpenAI + Gemini — LLM Layer
The LLM powers the conversational interface, destination research, itinerary generation, the 12-dimension flight scoring algorithm, loyalty program matching, and the quality-checking layer that validates whether agent outputs are coherent. We use a multi-model strategy: faster, cheaper models for simple extraction and classification tasks (parsing "next Friday" into a date, identifying airport codes from city names), and more capable models for complex reasoning tasks like multi-city itinerary optimization and natural language trip parsing.
Gemini serves as a fallback provider, ensuring that LLM-dependent features remain available even during OpenAI outages or rate limit events. Our circuit breaker implementation tracks failure rates per provider and automatically routes requests to the healthy provider when failure rates exceed a configurable threshold. This dual-provider strategy has maintained 99.9%+ LLM availability across the platform.
Stripe — Payment Processing
Altitude processes real payments for real flights. This is not a demo or a prototype. We use Stripe for all payment processing, with SetupIntents for saving payment methods, PaymentIntents for charges, and webhook-driven state management for asynchronous payment events. Stripe handles PCI compliance at the infrastructure level — card numbers never touch our servers. We detail the payment security architecture in the dedicated section below.
Key Engineering Challenges
Real-Time Flight Pricing and the Caching Paradox
Flight prices are one of the most volatile data types in consumer technology. A fare can change every few minutes based on demand, remaining inventory, competitive pricing algorithms, and dynamic yield management systems that airlines use to maximize revenue per flight. This creates a fundamental tension: users expect instant results when searching, but stale prices erode trust the moment they try to book ("Why does it show $450 but the airline wants $520?").
We considered three approaches. The first was no caching — hit the Duffel API on every search. This was immediately ruled out: API response times of 2-5 seconds per search, combined with rate limits, made this unworkable for a responsive user experience. The second approach was aggressive caching with long TTLs — cache results for hours and accept some price drift. This failed in testing because price discrepancies angered users and eroded trust more than slow results. The third approach, which we shipped, is a tiered TTL strategy with preference-aware cache keys and real-time revalidation at booking time.
Our caching strategy works as follows. Search results are cached in Redis with a short TTL (3-5 minutes) keyed by a normalized combination of origin, destination, dates, cabin class, and passenger count. Crucially, the cache key also includes user preference parameters — a user searching for "direct flights only" gets a different cache key than one who accepts connections, even for the same route and dates. This prevents cache pollution where one user's preferences skew results for another.
Offer details follow the airline's own expiry window (Duffel provides this per-offer), typically 15-30 minutes. Destination metadata — airport information, airline names, route existence — caches for 24 hours since it changes infrequently. LLM-generated content uses type-specific TTLs: destination descriptions cache for 1 hour, itinerary suggestions are tied to the underlying flight and hotel data TTL, and general travel advice uses longer TTLs with a version key for invalidation when our prompt templates change.
When a user proceeds to booking, we always re-validate pricing in real-time against the Duffel API before creating an order. The cached results are for browsing and comparison; the booking flow always uses live data. If the price has changed, we show the user the updated price and let them decide whether to proceed.
We also built a price-change notification layer. If a user has a trip in draft and we detect that one of the selected flights has changed price by more than a configurable threshold, we proactively notify them. This turned a potential trust issue into a feature that users actively value.
12-Dimension Flight Scoring
Traditional flight search returns results sorted by price or duration. Altitude scores flights across 12 dimensions to find the genuinely best option for each specific traveler. The scoring dimensions include: base fare, total journey duration, number of connections, layover quality (scored by airport amenities, lounge access, minimum connection time risk), departure time convenience (weighted by traveler preference for morning vs. evening flights), airline on-time performance, baggage inclusion, seat selection availability, fare flexibility (refundable vs. non-refundable), loyalty program alignment (whether the traveler earns miles in their preferred program), aircraft type, and carbon footprint estimate.
Each dimension is weighted based on the traveler's profile and stated preferences. A business traveler who values flexibility gets higher weight on fare flexibility and on-time performance. A budget traveler gets maximum weight on base fare with minimum connection time as a secondary sort. The weights are configurable per-user and per-trip, and the AI adjusts them dynamically based on conversational cues — if a user says "I don't care about the time, just find the cheapest option," the scoring rebalances automatically.
Multi-Provider Hotel Aggregation
Combining flight data and hotel data from different providers into a coherent trip plan is harder than it sounds. Each provider returns data in different formats, with different levels of detail, different cancellation policies, and different pricing structures (per-person vs. per-room, taxes included vs. excluded, resort fees disclosed vs. hidden). We built a normalization layer that transforms all provider responses into a unified internal schema before any of it reaches the LLM or the frontend.
This normalization is not just about data types — it is about semantics. When one provider says "non-refundable" and another says "no changes permitted," those mean similar but not identical things. When one hotel quotes $150 per night and another quotes $180 per night "including all taxes and fees," the actual cost difference may be smaller than it appears. We maintain a mapping layer that translates provider-specific policy language into a standardized set of attributes that the AI agents can reason about consistently, and we always show users the total cost including all known fees.
Hotel coordinates required particular attention. Many hotel listings in aggregator databases have approximate or missing coordinates, which breaks proximity-based search and map display. We built a coordinate enrichment pipeline that cross-references multiple data sources and geocoding services to maintain accurate location data for our 10,000+ hotel properties.
LLM Prompt Engineering for Structured Travel Output
Making an LLM reliably handle travel queries is surprisingly difficult. Dates are the biggest source of hallucination. A model might confidently tell you that "cherry blossom season in Tokyo is usually in June" (it is late March to mid-April). It might invent airport codes, suggest hotels that do not exist, or propose itineraries that are physically impossible given travel times between cities.
We learned early that the LLM should never be the source of truth for factual travel data. It reasons about the data, but every fact — every price, every flight time, every hotel name — comes from a verified API response. The LLM's job is synthesis and recommendation, not recall.
This required careful prompt architecture. We use structured function calling to ensure the LLM requests specific data from our APIs rather than generating it from training data. We validate every factual claim in LLM output against our provider data before presenting it to the user. And we built a feedback loop where detected hallucinations are logged and used to refine our prompts over time.
We also built a comprehensive prompt compression pipeline to keep token costs manageable. An unoptimized trip planning conversation can easily consume 50,000+ tokens. Through structural compression (compact JSON representations with abbreviated field names), semantic compression (summarizing older conversation messages into compressed context blocks while keeping recent messages in full detail), and selective inclusion (the flight agent does not need hotel details, and the itinerary agent does not need raw fare rules), we reduced average token consumption by roughly 60% without any measurable drop in output quality. That translates to approximately 200+ tokens saved per LLM call, which compounds to significant cost reduction at scale.
Through prompt compression, context windowing (keeping only the last 10 messages and summarizing older ones), intelligent model selection based on task complexity, and response caching for repeated query patterns, we reduced the average cost per trip plan from the equivalent of a full GPT-4 conversation to a fraction of that. The compression pipeline alone saves approximately 200 tokens per LLM call. For context, a single flight search result with 10 options, each with multiple segments, fare details, and baggage policies, can easily be 3,000 tokens before compression.
Group Travel: The Constraint Satisfaction Problem
Solo travel planning is hard. Group travel is an order of magnitude harder. Altitude's "Waves" feature enables multi-origin group coordination where travelers departing from different cities need to converge at a shared destination. Multiple passengers may have different passport nationalities (affecting visa requirements), different dietary restrictions, different airline loyalty programs, and different budgets. Coordinating schedules across a group — "these three people can only travel on weekends, but this person is flexible" — turns trip planning into a constraint satisfaction problem with a combinatorial explosion of possibilities.
We handle this by modeling each traveler as an entity with their own preference profile and constraint set. The orchestrator agent treats group trip planning as finding the Pareto-optimal solution across all participants' constraints. When perfect satisfaction is impossible (and it usually is), the system explicitly surfaces the tradeoffs: "This flight is $80 more per person but accommodates everyone's schedule" or "Two hotels are needed because no single property has enough rooms in the requested category."
The Waves system adds another dimension: travelers from different origin cities. A group of five friends in New York, Chicago, and Los Angeles all want to meet in Cancun for a long weekend. The system needs to find flights from three different origins that arrive within a reasonable window, coordinate hotel check-in times, and allocate the shared costs in a way everyone agrees on. This is where the DAG-based task orchestration architecture pays off — each origin becomes an independent flight search branch, and the orchestrator converges them at the destination node with temporal and budget constraints.
Refund Flow Complexity
Airline refund policies are extraordinarily complex. Partial refunds, refund-to-voucher, different rules for different fare classes, time-based eligibility windows, and airline-specific policies that change without notice. A flight booked on a "basic economy" fare might be non-refundable but allow a name change for a fee. A "flexible" fare might be fully refundable but only before 24 hours prior to departure. Cancel-and-rebook scenarios involve calculating the fare difference, applying change fees, and handling cases where the new fare is lower than the original.
We built a dedicated refund orchestration system with dead-letter queues for failed refunds, automated retry logic with exponential backoff, and a reconciliation pipeline that tracks refund status across both Stripe (payment reversal) and Duffel (ticket cancellation). The confirm-and-switch flow — where a user wants to change to a different flight — required particular care around refund-then-rebook atomicity to avoid scenarios where the refund succeeds but the rebooking fails, leaving the user without either ticket.
Security and Privacy Architecture
Altitude handles sensitive personal information (passport numbers, dates of birth, loyalty program details) and processes real financial transactions. Security is not a feature — it is a prerequisite. Our security posture was shaped by 35 production audit sessions that systematically tested every endpoint, data flow, and edge case through adversarial, multi-agent parallel audits.
PII Encryption
Traveler PII is encrypted at the field level using AES-256-GCM before being stored in PostgreSQL. Loyalty program numbers, passport details, dates of birth, email addresses, phone numbers, and contact information are never stored in plaintext. The encryption keys are managed separately from the database credentials, and decryption only occurs at the moment the data is needed for an API call to a booking provider — never for display in the UI, where we show masked values instead. GDPR data export functionality decrypts PII on-the-fly for the requesting user without persisting decrypted data.
Tenant Isolation and Access Control
Every database query is scoped to the authenticated user. There is no endpoint in the system where user A can access user B's trip data, payment methods, or traveler profiles — enforced at the API layer, not just the UI. Shared trip features use explicit invitation codes with timing-safe comparison to prevent enumeration attacks. Booking detail endpoints perform lightweight ownership checks before returning any data.
Payment Security
Every payment method is verified for ownership — a user can only charge a card they personally set up via Stripe's SetupIntent flow, enforced at the API level, not just the UI. We implemented idempotency keys on all payment operations to prevent double-charges in the case of network retries, and every charge is deduplicated atomically using Redis to prevent race conditions in concurrent requests. Webhook events from Stripe are verified using timing-safe signature comparison, and duplicate webhook delivery is deduplicated at the database level to prevent double-processing.
Rate Limiting and Abuse Prevention
Every public endpoint has rate limiting configured per-route, with tighter limits on authentication endpoints (login, registration, password reset) to prevent credential stuffing and brute-force attacks. We use a sliding-window rate limiter implemented in Redis Lua for atomic increment-and-check operations. Health check endpoints have separate, stricter rate limits to prevent monitoring tools from consuming API capacity. The rate limiter scales based on the user's plan tier, with higher limits for authenticated users.
Webhook Verification and Idempotency
Inbound webhooks from Duffel (booking status changes, price alerts) and Stripe (payment events) are verified using provider-specific signature verification. All webhook handlers are idempotent — processing the same webhook event twice produces the same result without side effects. We use database-level deduplication with a TTL-based cleanup to prevent the webhook event table from growing unbounded.
Across 35 production audit sessions covering 140+ files, we identified and resolved 200+ individual issues spanning security, performance, reliability, and correctness. Every silent error catch in the codebase (30+ instances) was replaced with structured logging. The final audit session achieved zero critical or high-severity issues remaining. This level of hardening is unusual for a startup-stage product, but we believe it is the minimum acceptable standard for software that handles real money and personal data.
Performance and Optimization
Caching Strategy Summary
The full caching architecture spans four tiers:
- Flight search results — 3 to 5 minute TTL in Redis, preference-aware cache keys that include cabin class, direct-flight preference, and passenger configuration. Short enough that prices are reasonably current, long enough that a user browsing through results does not trigger repeated API calls
- Offer details — TTL tied to the airline's offer expiry (Duffel provides this per-offer), typically 15 to 30 minutes. We add a small buffer before the actual expiry to avoid serving offers that expire during the user's review
- Destination metadata — 24-hour TTL. Airport information, airline names, route existence, and hotel coordinates do not change frequently
- LLM-generated content — Varies by type and temperature. Low-temperature outputs (deterministic responses) cache for longer periods. Destination descriptions: 1 hour. Itinerary suggestions: tied to the underlying flight and hotel data TTL. The cache is version-keyed so prompt template changes automatically invalidate stale content
Latency Optimization
The target for end-to-end plan generation is under 30 seconds, which is aggressive given that it involves multiple API calls to Duffel, hotel providers, LLM inference, and constraint solving. We achieve this through aggressive parallelization (the DAG orchestrator runs independent branches concurrently), streaming responses (the UI shows partial results as they become available rather than waiting for the complete plan), and intelligent prefetching (when a user mentions a destination in conversation, we start searching flights before they explicitly ask).
LLM Cost Management
LLM costs scale linearly with token count, and travel conversations are token-heavy. Beyond the prompt compression pipeline described above, we employ several additional cost management strategies: model cascading (try a cheaper model first and escalate to a more capable one only if the output quality is insufficient), response caching for repeated query patterns, and dynamic max-token budgets based on task type (a simple date extraction needs far fewer output tokens than a full itinerary generation).
We track per-request costs in real-time and enforce configurable daily spending caps with atomic Redis operations to prevent budget overruns during traffic spikes. Cost analytics are broken down by model, task type, and user tier, giving us granular visibility into where LLM spend is going and where further optimization would have the highest impact.
Social Features Architecture
Altitude is not just a booking tool — it is a social travel platform. Users can connect with friends, plan trips collaboratively, share reviews, and discover trips through an activity feed. The social layer required its own set of architectural decisions.
Friend connections use a bidirectional relationship model with request/accept flow. Connection requests are rate-limited to prevent spam, and the acceptance flow uses timing-safe token comparison to prevent enumeration. Shared trip visibility is carefully scoped — a user can only see trips that have been explicitly shared with them, and shared trip invitations mask email addresses of other participants until the invitation is accepted.
Group chat is integrated directly into the trip planning workflow, so travelers can discuss options and make decisions without leaving the platform. The chat uses cursor-based pagination with date validation to prevent injection of invalid cursor values.
Compatibility scoring analyzes travel preferences across connected friends to suggest who would be a good travel companion based on budget alignment, pace preferences (relaxed vs. activity-packed), accommodation style, and dietary compatibility. This scoring is computed asynchronously and cached, refreshing when either user's preference profile changes.
Trip reviews and activity feed let users share completed trip experiences with their network. Reviews include ratings across multiple dimensions (value, location, activities, accommodation) and the AI uses this review data to improve future recommendations for similar travelers.
Developer Platform
Beyond the consumer-facing product, Altitude exposes its capabilities through a developer platform. A REST API allows third-party applications to access trip planning, flight search, and booking functionality programmatically. We also built an MCP (Model Context Protocol) server that enables AI agents and LLM-powered applications to interact with Altitude's travel planning capabilities natively — an AI assistant in another application can plan and book trips through Altitude without any custom integration code.
The developer platform uses the same authentication, rate limiting, and audit logging infrastructure as the consumer product. API keys are scoped to specific capabilities (search-only vs. search-and-book) and rate-limited per tier.
Lessons Learned
Building Altitude through 35 audit sessions taught us more about production software engineering than any individual project before it. Here are the most transferable lessons.
Invest in Structured Logging from Day One
We spent significant time in later audit sessions retrofitting proper error logging into catch blocks that originally swallowed errors silently. We found and replaced 30+ silent catch blocks across the codebase — places where exceptions were caught and discarded, hiding bugs that only manifested as mysterious user-facing issues. Starting with a structured logging framework and a "no silent catches" policy from the beginning would have saved dozens of hours of debugging.
Design Cache Keys for Multi-Tenancy from the Start
Our initial cache key design did not account for user preferences, which meant that one user's "direct flights only" search could be served to another user who was fine with connections. We had to rebuild the caching layer to make cache keys preference-aware. Thinking about cache isolation early — not just user isolation, but preference isolation — would have been significantly cheaper than retrofitting it.
Build the Refund System Before You Need It
Airline refund policies are extraordinarily complex, and we underestimated this complexity. We ended up building a dedicated refund orchestration system with dead-letter queues for failed refunds, automated retry logic, maximum refund amount validation, and a reconciliation pipeline. Building that infrastructure in the initial architecture phase rather than bolting it on after launch would have reduced the number of manual interventions needed during early production.
Adversarial Testing is Not Optional for AI Products
For AI-powered products, adversarial testing is not optional — it is the only way to build trust. Users will try inputs you never imagined. LLMs will occasionally produce outputs you never expected. You have to assume both will happen and build your system to handle it gracefully.
Our multi-agent parallel audit methodology — where multiple reviewers simultaneously attempt to break different parts of the system — found classes of bugs that sequential code review would have missed. Race conditions in payment processing, TOCTOU vulnerabilities in subscription changes, and PII leaks in error responses all emerged from adversarial parallel testing.
Fail Closed on Cache Reads
When a cache read fails (Redis connection timeout, corrupted data), the system should fall through to the source of truth, not return an error to the user. But it also should not silently serve potentially stale or corrupt data. We implemented a fail-closed cache read pattern: on any cache anomaly, the system logs a warning, bypasses the cache, and fetches fresh data from the provider API. The user experiences slightly higher latency but never sees incorrect data.
What's Next
Altitude is live and production-ready at altitudetravel.io, but we see it as the beginning of what AI-native travel can be, not the endpoint. The underlying architecture — multi-agent orchestration with real-time provider integration — is a pattern we believe will define the next generation of travel technology.
We are continuing to expand in several directions. Provider coverage is an ongoing investment — more airlines, more hotel aggregators, and more activity providers to give users the widest possible selection. Multi-modal itineraries that incorporate trains, ferries, and rental cars alongside flights will enable more complex trip types, especially in regions like Europe and Southeast Asia where ground transportation is often superior to flying. Deeper loyalty program integration will help travelers maximize the value of their existing points and miles across planning and booking decisions.
On the AI side, we are exploring proactive travel recommendations — where the system suggests trips based on a user's past travel patterns, stated bucket list destinations, and real-time deals — and continuous trip optimization that monitors booked trips for price drops, schedule changes, and upgrade opportunities. The goal is to make Altitude not just a planning tool but a persistent travel intelligence layer that works for you between trips, not just during them.
The days of manually comparing dozens of tabs to plan a trip are numbered. We are building the replacement.
Frequently Asked Questions
What tech stack does Altitude use?
Altitude is built on Next.js 14 (App Router with Server and Client Components), Prisma ORM with PostgreSQL, Redis for caching and rate limiting, the Duffel API for flight booking across 500+ airlines, OpenAI and Gemini for LLM-powered planning, and Stripe for payment processing. The entire codebase is TypeScript with strict type checking.
How does Altitude handle real-time flight pricing?
We use a tiered Redis caching strategy with preference-aware cache keys. Search results cache for 3-5 minutes, offer details follow airline-provided expiry windows (15-30 minutes), and destination metadata caches for 24 hours. Booking flows always re-validate pricing in real-time against the Duffel API before creating an order, so users never pay a price different from what they confirmed.
Why did Altitude choose Duffel over a traditional GDS?
Duffel provides a modern REST API with real-time pricing and actual ticketing capability, enabling integration in weeks rather than the months required by traditional GDS providers like Amadeus or Sabre. While GDS connections offer marginally broader airline coverage, Duffel's developer experience and integration velocity were decisive advantages.
How does the AI avoid hallucinating travel data?
The LLM never serves as the source of truth for prices, flight times, or hotel availability. It uses structured function calling to request data from verified APIs and every factual claim is validated against provider data before being shown to users. The LLM's role is reasoning and synthesis, not factual recall.
How does group travel work with different origin cities?
Altitude's "Waves" feature models each traveler as an entity with their own preference profile and constraints. The DAG-based orchestrator runs independent flight searches per origin city and converges them at the destination with temporal and budget constraints. When perfect satisfaction is impossible, the system surfaces explicit tradeoffs for the group to decide on.
Try Altitude
Experience AI-powered travel planning. Plan your next trip through conversation, with real-time flight booking across 500+ airlines, 10,000+ hotels, and personalized itineraries generated in under 30 seconds.
Visit Altitude