Inspiration

Every budgeting app gives the same advice: "You spent $540 on dining. Try cooking at home." That's technically correct and practically useless. People don't overspend on food because they love spending money. They overspend because they love those places.

I wanted the opposite of a budget app: something that keeps your taste and changes the bill. Not "spend less," but "go here instead." Answering that well needs something an LLM alone doesn't have: real knowledge of what people with your taste actually love, place by place, city by city. That's exactly what Qloo's taste graph is.

What it does

Pareil (French for "the same") reads where you spend and finds the cheaper twin of the places you love.

  1. Upload a month of spending (a simple CSV: date, merchant, amount, city), or open the NYC or Bengaluru demo in one click.
  2. The agent tidies up. Bank strings like CARBONE NYC 0234 become real places; Uber, Amazon and utility bills are set aside.
  3. Qloo finds taste twins. For each place you spend a lot at, Pareil asks Qloo for places your taste loves that are exactly one price tier cheaper.
  4. Quality rules filter the results. A twin must share a specific cuisine and must be rated as well or better than the original.
  5. You get swaps, explained. For example: "Your Carbone visits point here: Ci Siamo, Italian, a tier cheaper, rated 4.6." Each swap comes with an honest monthly saving estimate based on your own spending.

When nothing cheaper is good enough, Pareil says so: "Hard to beat: rated 4.7, nothing cheaper matched it." It never forces a worse swap to make the numbers look bigger. Every Qloo call is listed in a "How we know" panel, and live runs stream as an agent timeline.

On the NYC demo month, Pareil finds swaps for 4 places and estimates ~$494/month if you swap 1 in 2 visits, clearly labelled as an estimate.

How we built it

Qloo is the core. Remove it and all Pareil can say is "spend less."

  • /search resolves cleaned merchant names to Qloo place entities (price level, rating and tags in one call). Results without a food tag are rejected, so "Carbone" never matches Carbone Memorials.
  • /v2/insights (filter.type=urn:entity:place) finds the twins, with:
    • Weighted signals via POST: the origin place (weight 3) plus the user's two most-visited other places (weight 1 each). Equal weights drifted toward cheap casual spots; weighting keeps twins faithful to the original while still personalising them.
    • filter.price_level.min = filter.price_level.max = tier − 1: exactly one tier cheaper.
    • A food tag filter (urn:tag:genre:place:restaurant), which removed salons, gyms and a clinic from the candidates.
    • feature.explainability=true: tells us which of your places drove each match, so every explanation is grounded in Qloo's own attribution.
    • filter.location.query keeps twins in the user's city.

The agent: the model chooses, the code enforces.

  • The LLM cleans merchant names (strict JSON, validated with Pydantic, with retries), decides how to recover when matches are thin, and writes the short "why" for each swap.
  • The code decides everything that must be trustworthy:
    • Recovery options are computed from the rejection reasons. Candidates failed on cuisine → the agent may broaden_signal or widen_tags. Too few one tier down → allow_two_tiers_down (labelled in the UI). Failed on rating → the only option is give_up. The model chooses only among what the code offers.
    • Deterministic ranking: score = (rating / 5) × (0.5 + 0.5 × overlap of specific cuisine tags). Broad tags like "American" don't count on their own; "restaurant" and "beer and wine" are ignored.
    • Name guard: an explanation may only mention places Qloo returned; anything else is rejected and replaced with a template.
    • Dedupe (each twin shown once), plus hard limits on iterations, Qloo calls and LLM retries. Every step is recorded in a trace.

The savings estimate. Qloo gives price tiers, not menu prices, so the money comes from the user's own data:

$$ \text{saving} = \text{visits per month} \times \big(\text{your average bill at the place} - \text{your average bill at the twin's tier}\big) $$

It's clamped so a saving never exceeds what you spent there. If you already spend like a lower tier somewhere (lunch at Katz's, not dinner), the saving is $0 and it says so. Thin tiers fall back to a default average marked estimate. The headline assumes you swap 1 in 2 visits, not all of them.

Stack.

  • Backend: Python + FastAPI, split into small modules (cleaning, resolution, tiers, twins, ranking, savings, agent). Live runs stream over Server-Sent Events.
  • LLM: Google Gemini, called directly over HTTP with a small wrapper, so swapping providers is a config change. A rule-based fallback cleaner takes over if the model times out.
  • Frontend: static HTML/CSS/JS, no framework, with a custom dark design and an animated preloader.
  • Hosting: frontend on Vercel, API on Render's free tier. The demos are recorded runs served statically (trimmed to the fields the UI shows), so they're instant even while the backend sleeps; "Run live" wakes the API and calls Qloo for real.

Challenges we ran into

  • The docs didn't match the API. The documented tag ID example didn't match real responses, and the documented format for weighted signals returned a 400 error. I found the working formats by testing against live responses and recorded every finding in the repo.
  • "Cheaper" kept turning into "worse." Early versions replaced an Italian dinner with $ ramen, suggested a cocktail bar instead of a restaurant, and once "recovered" by lowering the rating floor. Each broke the product's promise, so I added the exact-one-tier filter and the specific-cuisine check, and removed rating relaxation entirely.
  • The first savings numbers were wrong. Using tier averages instead of the user's own bills overstated one saving to more than the user had spent there. Fixing the formula cut the headline from ~$1,310 to ~$494 a month: less impressive, and actually true.
  • Affinity barely varies (~0.97 for every candidate), so it can't rank twins on its own. Cuisine overlap and rating do the real work, and the report shows affinity transparently.
  • Practical issues: a Windows security policy blocked a compiled dependency of the LLM client library, so I replaced it with a ~30-line HTTP wrapper; LLM latency ranged from 4 to 60 seconds (one batched call, timeouts and fallbacks); and the free backend sleeps after 15 minutes (static recorded demos plus a wake-up flow).

Accomplishments that we're proud of

  • Every swap keeps its promise: same specific cuisine, exactly one tier cheaper, never rated lower. When that's impossible, Pareil says "Hard to beat" instead of cheating.
  • An agent you can audit: every decision is in the trace, every Qloo call is listed, and no place name in an explanation can be invented.
  • Honest money: savings come from the user's own bills, are capped by what they spent, and are labelled as estimates.
  • 69 automated tests. I deliberately broke each rule to confirm its test catches it.
  • A complete, hosted product with instant demos, live streaming runs and a mobile-friendly design, built solo.

What we learned

  • An LLM is the right tool for messy input and explanations, and the wrong tool for decisions that must be trusted. Splitting those responsibilities made the agent both smarter and safer.
  • Verifying an API against live responses beats trusting its examples.
  • Honest numbers are a feature. "Hard to beat" and "estimate" labels build more trust than a big headline figure.

What's next for Pareil

  • More categories through the same adapter design: hotels (Qloo has hotel price data), and fashion once product prices are available.
  • Bank-statement import (PDF, and UPI SMS in India) instead of CSV.
  • A "Not my vibe" feedback loop that learns which swaps a user accepts.
  • Better Bengaluru coverage for premium places.

Try it: https://pareil-eta.vercel.app (demos are instant; "Run live" may take up to a minute to wake the free backend) · Code: https://github.com/Ashwanijha1405/Pareil (MIT)

Built With

Share this project:

Updates

Submission history