Inspiration

Ask an LLM for places in Tokyo and you get twelve good ones. It isn't wrong — it's just the same twelve everyone gets. It has no idea what fits you, what's a forty-minute train ride away, or which of them you should skip entirely.

Travel is where that costs you something real: a wasted afternoon, a dinner you didn't enjoy, a city you leave feeling like you saw the inside of cafés. So we built the thing a list can't be: a route.

What it does

You give Thread a city, how many days, a budget tier, and what you actually care about — indie rock, specialty coffee, vinyl, ramen, galleries, natural wine.

It gives you back a day-by-day itinerary, where each day stays in one walkable neighbourhood. Stops are ordered morning → afternoon → evening, with travel times between them, and each one shows how many of your interests it covers ("2 of 3").

Then the part we're most attached to: what it skipped, and why. "In a neighbourhood that isn't on your route." "This slot went to a better fit." A recommendation is only as trustworthy as its exclusions.

Every place comes from Qloo. There is no hardcoded catalogue.

Live at: http://136.110.21.121/

How we built it

Node 22 + TypeScript, Express, React/Vite, deployed in Docker on a GCP VM.

The pipeline:

  1. Resolve — each interest becomes either an entity anchor (jazz → Miles Davis, vinyl → Blue Note Records, via qloo api search) or a place-category tag (coffee → urn:tag:category:place:coffee_shop, via qloo exec find_tags).
  2. Discover — one qloo api insights call per interest, some with --signal-entities, others with --filter-tags. One call per interest is deliberate: a merged call returns better places but loses attribution, and "2 of 3" would be a lie.
  3. Merge by entity id. A place surfaced by two interests is genuinely a two-of-three match.
  4. Filter on Qloo's primary_genre, not its tag list.
  5. Cluster by distance — anchored on the densest cluster, dropping anything past 12 km, because --filter-location covers a whole metro region.
  6. Schedule stops into morning/afternoon/evening, exhaustively.
  7. Choose days — one neighbourhood per day, penalising repeated shapes, so a week doesn't become the same three hours seven times.

Separately, the interest filters adapt per city: one insights call with all twelve tags OR'd across 30 places measures what actually exists there, cached for 30 minutes. Lisbon reports wine 27, coffee 24, art 16. Reykjavik reports coffee 6.

Challenges we ran into

Artist anchors return landmarks, not venues. "Haruki Murakami" gave us churches, museums and a viewpoint. "Radiohead" gave us museums. The graph relates an audience to cultural places generally, which is true and useless. Most interests had to become category tags instead.

Tags are not categories. Every Qloo place carries ~100 loose audience tags, so "any tag contains book" matched a theatre. primary_genre — Qloo's own category field — fixed both the filter and the venue label.

Metro leakage. A Lisbon search returned Belém, Cascais and Cabo da Roca, towns up to 40 km away.

Missing price data. Only 3 of 20 places in Lagos have a price_level. Treating unknown as mid-range silently deleted them the moment you picked "Budget". Unknown is now always allowed, and the count is reported.

Ten cities had no data at all. We swept 120 candidates and found Copenhagen, Edinburgh, Shanghai, Chengdu and six others return zero places from Qloo. We removed them rather than ship options that fail. 89 of the 120 were healthy, with a mean sample of 36.

Cold start was 35 seconds. Resolving twelve interest tags meant twelve Node CLI subprocesses on a 2-vCPU VM, pushing memory to 1.2 GB. The tag map never changes, so we shipped it with the code — cold probes dropped to 4.2 seconds.

Accomplishments that we're proud of

  • A real itinerary, not a list: deterministic, walkable, ordered by time of day.
  • The skip list. It's the feature that makes the rest believable.
  • City-adaptive filters measured from the API instead of curated by hand.
  • A 110-city catalogue that we pruned with evidence rather than guesswork.
  • Pinned the runtime to Node 22.23.4, the release patching CVE-2026-111497 ("Node2Shell"), because a public demo shouldn't run on a vulnerable runtime.

What we learned

Qloo is an affinity graph, not a directory. The same request phrased as "who likes this" versus "what is this" returns completely different worlds — so most of the work was choosing the interface, not writing code.

We also learned that a recommendation's credibility comes from what it's willing to discard. Adding the skip list changed how people read the whole page.

And that 2 vCPUs change architecture: bundle anything static, cache anything repeated, and never fan out twelve subprocesses because you can.

What's next for Thread — Qloo route planner

  • Opening hours. Time windows are inferred from Qloo's time-of-day tags today. Real hours would let us stop saying "confirm before you go".
  • Multi-city trips, so a week across two cities becomes one thread.
  • Replace the last artist anchors with audience-overlap signals, which is what the graph is actually best at.
  • Cache finished plans per city + interests so a repeat demo is instant instead of ten seconds.

Built With

Share this project:

Updates

Submission history