ROAM: The model never makes up a number
Travel ranking where deterministic engines compute every score and fact, and the language model may only explain them, with its citations checked against the evidence it was given.
Solo — domain model, scoring engines, database, web and mobile clients · 2026
In development. The deployed web build shows sample opportunities because its ranking API is not connected to a database yet.
The problem
Travel recommendations are a natural fit for a language model and a bad place to trust one. A model will happily say a flight costs $412 and March weather is perfect, and nothing in the answer shows which of those it looked up.
Prices, availability, entry rules and weather change daily. A ranking that mixes those with a model's guesses cannot be reproduced, cannot be audited, and cannot be defended when someone books on the strength of it.
So ROAM draws one line and builds everything around it: engines compute, the model explains.
Architecture
Every number on screen comes from the dark boxes. The model only sees them after they exist.
- Traveler message, turned into a structured intent
- Provider observations, normalized with timestamps and provenance
- Hard constraints: budget, access, passport, flight time. Failures are excluded with reasons
- Personal Fit (55%) from Travel DNA, with its own confidence
- Current Opportunity (45%); missing inputs are dropped and the rest renormalized
- ROAM Score plus confidence, versioned and reproducible from its input snapshot
- Why Now, ranked by impact times confidence
- Model explanation, then a sanitizer: citations must match grounding keys, actions need confirmation
Key decisions
An ineligible destination is excluded, not scored low
If a place is over budget or needs a visa the traveler cannot get, it does not appear with a 41. It is removed before ranking and the reason is kept. A low score invites the reader to weigh it; an exclusion says it was never an option.
Missing data is missing, not neutral
When an opportunity signal has no observation, that component is left out and the remaining weights renormalize. Filling the gap with a middling value would quietly pull every sparse destination toward the average. Confidence drops instead, and it sits next to the score, because a 92 at 0.45 confidence and a 92 at 0.94 are different claims.
Citations are checked against the evidence the model was given
The explanation step receives a grounding bundle where every fact has a key. Anything the model cites that is not in that set is removed before the answer is shown. An explanation request with no scored opportunities behind it is refused rather than answered from general knowledge.
The model can propose, never commit
A model-suggested change to a trip always comes back with requiresConfirmation set, whatever the model asked for. Booking is outside automatic execution entirely. That rule is enforced in the sanitizer, not the prompt, so a clever reply cannot talk its way past it.
Every score can be rebuilt
A persisted score run stores its eligibility decision, component scores and confidence, constraints, Why Now evidence, input snapshot, scoring version and timestamps. Generation records keep the model, prompt version, intent, input hash and grounding keys, and deliberately not the model's hidden reasoning.
Numbers
- 55 / 45 — Personal Fit / Current Opportunity (the whole weighting, in the open)
- 25 — destinations in the seed corpus (for coverage checks; facts need sourced ingestion)
- 14 — database migrations (travel graph, observations and persistence)
- 0 — numbers the model may originate (fares, weather, scores, availability)
Nothing here has ranked a real traveler yet. The deployed API returns discover_unavailable until its database is connected, the web build's opportunity cards are hardcoded samples, and the repository has no automated test files yet. The architecture is written down phase by phase; proof that it holds up under live provider data is still ahead.
Web build (sample data)