If you've ever wondered "wait, why did Fareduck pick this fare over the other one?" — this post is the answer.
We score every flight offer we find through a deliberately two-stage process: a deterministic heuristic ranker first, then an optional LLM-based ranker on the survivors. Both are completely walled off from anything monetization-related. Here's exactly what each step does.
What "a deal" actually means at Fareduck
A deal isn't the cheapest fare. A deal is a fare that scores well on all four of these at once:
- Price — meaningfully below typical for the route, normalized for season and dates.
- Routing quality — direct beats connecting; reasonable layovers beat 23-hour torture connections.
- Persona fit — matches what you said you like (and avoids what you said you don't).
- Trust — a recognised carrier on a real route, not a fly-by-night charter doing one segment a week.
A flight at $239 from Denver to Lisbon with a 14-hour Frankfurt layover on an obscure carrier isn't a deal. It's a flight you'd cancel after booking. We don't surface that.
Stage 1: the heuristic ranker
Every offer that comes back from our flight-data provider runs through scoreOffers() — a pure deterministic function that produces a number from 0 to 100. No randomness, no machine learning. Same offer → same score, every time.
The factors it weights, ranked by influence:
| Factor | Weight | Why |
|---|---|---|
| Price vs typical for route | High | The whole point |
| Direct vs 1-stop vs 2-stop | High | Routing quality matters as much as price |
| Loyalty carrier match | High | Status is worth real money to people who have it |
| Preferred-airline match | Medium | Softer signal than loyalty |
| Reasonable total journey time | Medium | We penalize anything over 24 hours |
| Excluded-airline match | Hard exclude | We never surface what you said no to |
We deliberately don't weight things like:
- Frequent-flyer-mile earning potential (proxied through loyalty match)
- Cabin class (we only search Economy at MVP)
- Booking site (it's never a factor in ranking)
- Affiliate commission (this would be a betrayal of trust)
The heuristic ranker's job is to throw out the obvious junk and keep the top ~15 candidates per user.
Stage 2: the LLM ranker (when we can afford it)
Heuristics don't catch nuance. "This person said they like quiet walking cities and we just found three options to Lisbon, three to Naples, and three to Bangkok — which 7 should we feature?" That's a question a heuristic can't reason about.
For the top candidates, we hand the list to Gemini 2.5 Flash with the user's full persona summary as context. Gemini reorders the list and writes a one-sentence rationale per pick — the "why we picked this" line you see on your digest.
We use the LLM judiciously:
- It runs after the heuristic ranker, not before. The heuristic filters the slush; the LLM polishes the top.
- It has a strict daily call budget per user. If we're over budget, we fall back to heuristic-only ranking and a generic rationale. Your digest still goes out.
- It never knows about affiliate commissions. The prompt doesn't mention them. The candidate list it sees doesn't include them. There's no path for "Skyscanner pays us more than Kiwi" to influence the ranking, by design.
The architectural wall
This is the part I'm most proud of and the part most easily overlooked.
Inside our code, the ranking module (packages/domain/ranking/) is structurally forbidden from importing anything from the affiliate, ad, or flight-provider modules. An ESLint rule enforces it. If a future engineer tries to add "boost deals from partners that pay us more," the build fails.
Same wall in the other direction: the affiliate module produces booking links after the ranking is done. It doesn't see scores. It can't bid for placement.
We do this because there's no other way to make trust real. You can't tell whether a recommendation is honest from a privacy policy paragraph. You can tell by the architecture, and you can read our code.
What this means in practice
When you open your Sunday digest:
- The order is set by score, not who paid us.
- The rationale line was written either by the LLM (no commission info) or generated from the heuristic reason codes (no commission info).
- The "Search this fare" button routes through whatever booking partner has the best deep-link match for that itinerary — picked by
selectPrimary(), which considers brand recognition, commission rate, and free-cancellation status. The deal we showed you was already chosen by the time this step runs.
If we can't find a partner-link match, we fall back to a Google Flights deep-link. No commission. We still show the deal.
When this breaks down
It's worth being honest about edge cases:
- Cold-start users (no persona yet) get heuristic-only ranking until they finish onboarding.
- Rare routes (Boise → Hanoi) sometimes have only 1-2 offers; we surface what we find, even if the score is mediocre.
- Sandbox data: until our Duffel live token is approved, the prices you see are deterministic test fixtures — directionally correct, not actual booking prices. We label this clearly on the deals page when sandbox mode is on.
- LLM downtime: when Gemini is rate-limited (it's a free tier we're on), we ship heuristic-only digests. You'll see slightly more generic rationales those weeks.
I'll write a follow-up post when we add real-time price re-verification at click time (post-Duffel-live approval).
— The Fareduck team