7.5% Cashback Isnβt a Price. I Built TripStack to Read the Rest.
What I Built
"Earn 7.5% cashback." That sounds like a number you can subtract from a hotel bill. Then you read the next sentence: maximum reward $60. Minimum spend $200. Taxes excluded. Cannot be combined with coupon codes. Now compare it with another hotel offering an instant discount. Which costs less? Which requires less money today? Does either stay put you near the part of town you actually wanted? A headline rate gives you homework.
I built TripStack, a hotel-cost comparison prototype that uses local Gemma to help turn promotional terms into conditions you can inspect, then passes those conditions to a deterministic pricing engine. The interface separates upfront payment, estimated cashback, and estimated net cost. You can plan multiple destinations, filter stays by location, review an offer's source evidence, and preview its effect before applying it.
For Slow and Steady, the task I wanted to take off someone's plate was the repeated cross-checking: remembering a cap, finding an exclusion, comparing a delayed reward with an immediate discount, and doing it again for the next city. The user still makes the decision. The app organizes the evidence and does the arithmetic.
About the demo
- Hotel records, quotes, coordinates, and promotion terms are simulated examples.
- Gemma inference, source checks, geographic filtering, and price calculations actually run.
- The displayed savings are not evidence of real market savings.
- An Amadeus adapter is included, but I have not validated production quote retrieval.
Demo
The recording starts with five nights in Boston. I inspect the recommendation, analyze a cashback offer with Gemma, review the evidence, and apply it. Then I add five nights in New York and repeat the comparison.
On the Boston demo snapshot, the estimated net cost changes from $731.88 to $712.75 after applying the reviewed offer. That $19.13 difference is a calculation on demo data, not a claim about a currently bookable hotel or a guaranteed payout.
Applying a promotion does not fetch a fresh hotel price. Both sides of the preview use the same quote snapshot, so the difference reflects the promotion calculation rather than a moving baseline.
This screenshot shows a separate three-city example. Demo labels and budget warnings remain visible.
Code
GitHub: michelleeyin/tripstack
- The application code is MIT-licensed.
- The repository includes the demo fixtures, prompts, pricing engine, tests, and local setup instructions.
- Gemma is an open-weight model with its own license; the application's MIT license does not replace it.
How I Built It
The most useful moment in the demo is a correction
In the recording, Gemma returns fixed_amount=60.0 for an offer whose sixty dollars is a cashback cap. Sixty is present in the source. Its role is wrong. That is a more interesting failure than a completely invented number: checking whether a number occurs in the text would not be enough.
In the hybrid parser, numeric values come from explicit rule matches. Gemma supplies other extracted conditions, with explicit source exclusions taking precedence. The discrepancy is recorded, the incorrect fixed-amount assignment is replaced with the rule parser's empty value, and sixty remains the cap. The warning is visible in the extraction record. It is part of the result, not something hidden to make the demonstration look cleaner.
A small model with a bounded job
TripStack runs Gemma through Ollama, with gemma:2b as the default. I used local CPU inference on a 16 GB Mac for the working demonstration. I did not fine-tune the model.
- The client requests structured JSON, validates the response with Pydantic, limits input and output length, and records inference timing and token counts when available.
- The prompt treats promotion text as untrusted data and tells the model not to follow instructions embedded inside it.
- The extraction path also checks source numbers and dates.
- Unsupported or unresolved benefits are excluded from reported savings.
- If inference fails, the app records a rule-parser fallback instead of calling the result a successful Gemma extraction.
Three parsing modes
| Mode | Numeric fields | Other conditions |
|---|---|---|
| Hybrid | Rule matches from the source | Model-assisted extraction, with explicit source conditions taking precedence |
| AI | Model output, subject to source checks | Model output, subject to validation |
| Rules | Rule parser | Rule parser |
These checks reduce specific failure modes. They do not prove that every interpretation is correct. A source excerpt can support a number without establishing a traveler's eligibility. That is why review remains visible, and why checking "I reviewed the source terms" does not upgrade an estimate into a verified benefit.
The model has a narrower responsibility than "find the cheapest trip." It proposes structured conditions that the rest of the system can examine.
The arithmetic has one implementation
The pricing engine uses Python Decimal, with amounts rounded to cents and rates retained to six decimal places.
Upfront payment = room rate + taxes + mandatory fees β instant discounts
Estimated cashback = eligible spend Γ rate, subject to the offer cap
Estimated net cost = upfront payment β estimated cashback
There is no second, approximate reward calculator inside the optimizer. Promotion selection prices candidate sets through the same engine used to display the bill. That matters when the user changes priorities.
In a controlled $100 example, a $50 cashback offer gives a lower estimated net cost, while a $30 instant discount gives a lower upfront payment. Both can be sensible choices. The selected objective must determine which one wins.
A regression also caught a quieter mistake: quantizing a rate like money turned 0.075 into 0.08. On $1,000, that changes $75 into $80. Rates and amounts now have separate precision rules.
Another distinction is between an estimated benefit and a verified one. An unverified instant discount can appear in an estimated calculation, but it must not reduce the cost calculated using verified benefits only. Those distinctions are easy to lose behind one attractive total. TripStack keeps them separate.
Location belongs in the comparison
A cheap room outside the user's chosen radius is not an eligible substitute. TripStack applies geographic constraints before ranking prices. It uses Haversine distance around a named downtown reference or user-supplied coordinates.
- Missing hotel coordinates fail an active radius filter.
- Savings baselines use the same location and refund constraints as the recommendation.
- Those distances are straight-line measurements, not walking times.
- A typed place name is a label, not an automatic address lookup.
- If no stay meets the constraint, the app should say so rather than quietly recommend somewhere else.
A simpler interface, with the evidence underneath
The main flow has three views:
- Your trip: enter preferences and inspect recommendations.
- Add an offer: parse terms, review evidence, and preview the effect.
- Inside TripStack: inspect extraction records and diagnostics.
The first result shows the decision-relevant numbers. Detailed calculations, alternatives, and model settings sit behind expandable sections. I wanted someone to understand the recommendation without reading a diagnostic report-and still be able to open that report when a number looked wrong.
Verification beyond a green model badge
The current suite has 109 passing tests, covering pricing, eligibility, extraction checks, model failures, geography, and page interactions.
- Automated model tests use mocked responses.
- Actual local inference was checked separately and appears in the recording.
- I also installed the project in a clean virtual environment and exercised the demo search in the browser. That validates the installation and workflow, not production travel data.
Even the diagnostics distinguish three states:
- The server is reachable.
- The model is installed.
- An inference request has actually succeeded.
Those are different claims.
Why Does Open Innovation Matter?
For this project, running the model locally changes what I can inspect and control.
- The offer text goes to my local Ollama instance rather than a hosted model API.
- I can change the prompt and output schema, inspect raw extraction records, switch installed models, and compare model-assisted parsing with the rule-only path.
- Once dependencies and model weights are installed, local extraction and fixture-based comparisons do not need a hosted inference service.
- Live hotel retrieval would still need network access.
- There is no per-request hosted-model bill in this setup, although local compute and electricity still have costs.
There are trade-offs, too: model downloads, memory use, startup time, and hardware compatibility become my responsibility. CPU mode helped make the demonstration work on my laptop; it did not make those constraints disappear.
Keeping the model and validation code locally inspectable makes the correction in the demo reproducible. Someone else can examine the prompt, replace the model, or challenge a pricing assumption. The open-weight model makes local inference possible. The open application code lets others inspect what happens to its answer.
What I Would Build Next
The largest unfinished piece is real quote retrieval. I would validate the existing provider integration, establish its coverage and price semantics, and clearly distinguish sandbox responses from production quotes before presenting TripStack as a live comparison service.
I would also expand evaluation beyond short English examples to longer promotions with membership, regional, and payment-card conditions. The small historical benchmark in the repository is a debugging aid, not a general accuracy claim.
I have not measured user time savings or validated the prototype in real bookings. The current build demonstrates a narrower workflow: read a promotion locally, inspect its conditions, and compare its effect on a fixed set of quotes.
A traveler should be able to stop thinking about a cap or an exclusion without losing the ability to check it. That is the kind of laziness I wanted to make possible.
Prize Categories
Best Use of Gemma - local Gemma inference through Ollama for promotion-term extraction, paired with source checks, explicit fallback records, and a review-before-apply workflow.
Comments
No comments yet. Start the discussion.