Inspiration
Every "moving to a new city" story starts the same way: you open a maps app, type "best nightlife", and get the same top-10 list everyone gets. None of it knows that home was Afrobeats on random Tuesdays, 2am shawarma, and rooftop sunsets. ELSEWHERE started from a simple frustration: recommendation tools optimize for popularity, not for belonging. We wanted the opposite tell the app what home feels like, and it finds where people with that exact taste already gather in the city you just landed in, with the reason attached.
What it does
- Onboarding wizard one taste scene at a time (Music → Food → Screens → Vibes), each with curated chips and a search box that resolves any free text an artist, a dish, a brand into real taste-graph entities. A "people into your picks also like" row refreshes live as you select, pulling cross-domain suggestions (pick Miles Davis, get John Coltrane, Amy, Ableton, and a jazz festival). Any city on earth works: a 169-city dictionary answers instantly, everything beyond it is geocoded on the fly.
- The honest side-by-side in chat, the same LLM answers the same question twice, streamed live: on the left with zero taste data (the generic list you'd get anywhere), on the right grounded in the taste graph a Leaflet map with real pins, five place cards with affinity multipliers ("1.3× your graph"), and the taste path connecting this you → this spot. A disclosure shows the actual query and the receipts (entity IDs), and an honesty toggle names the model writing both panes.
- Refine in place "more late-night", "cheaper", or a follow-up message re-runs the one result instead of stacking walls of text. Your session (picks, city, result) survives a reload; anything you pick is yours to remove.
How we built it
- Qloo does the grounding.
/searchpowers taste autocomplete and city resolution;/v2/insightswithsignal.interests.entities/signal.interests.tagsreturns the correlated places, brands, movies, shows, and neighborhoods behind every suggestion fan-out across five domains in parallel, merged round-robin so no category gets drowned out. Chip entities are probe-validated against the insights graph and carry steering tag IDs so taste survives even when a search-only entity isn't in the graph. - AG-UI 1.0 over hand-rolled SSE. The backend streams the agent loop as real protocol
events
RUN_STARTED,TOOL_CALL_START/ARGS/END/RESULT,STATE_DELTA, streamedTEXT_MESSAGE_*,RUN_FINISHEDso the UI shows every tool call as it happens. A second OpenRouter lane streams the generic (no-taste-data) answer concurrently via a thread + queue, draining progressively between tool events. - FastAPI + React/Vite + beUI. A never-500 backend: mock mode with committed fixtures when keys are absent, a three-rung fallback ladder that degrades visibly ("broadened search") instead of failing, and a sha256 cache for repeat queries. The frontend is a custom deep-dark monochrome theme (Inter + JetBrains Mono, glow focus, streaming carets) with Leaflet maps. 119 backend tests guard the contracts, and the whole thing is deployed on our own VPS behind Caddy at elsewhere.techa.pro.
Challenges we ran into
- Search ≠ graph. Qloo
/searchhappily returns entities the insights graph doesn't know and silently drops them as signals (our Afrobeats anchor vanished into generic results). We solved it with graph probes, probe-validated chips, and tag-signal steering, then reused the same trick when some IDs hard-400'd whole correlation domains. - Explainability never shows up live. The API returns popularity and ordering, not the per-entity scores we designed badges around so affinity now derives from real popularity against the result-set median, which is why badges actually vary (0.6×–2.4×).
- A grumpy graph under burst load (autocomplete firing right before a run) taught us to stop retrying identical calls, back off, and let an idle watchdog not a hard timeout judge stream health.
- Mojibake brand names, fabricated pins, flat 1.0× badges each got a root-cause fix
(byte-level repair, real
entity.location+ addresses, popularity-derived scores). - Streaming two LLM answers inside one synchronous generator took a thread + queue and a bounded progressive drain so a slow first token can't stall the grounded pipeline.
Accomplishments that we're proud of
- The comparison is honest by construction: same model, same city, one pane with the taste graph and one without and the UI says so. Watching the generic side type the obvious answer while the grounded side queries a live graph is the demo.
- Every recommendation carries receipts: the taste path that explains it, the affinity number, and the raw entity ID.
- Nothing in the product crashed in testing degradation is visible, never fatal backed by 119 passing tests and a live deployment on our own infrastructure.
What we learned
Grounding changes trust more than model quality does: the identical LLM reads completely differently with and without the taste graph. The API's warnings and quirks are data probing beats assuming. And honest UX (naming the shared model, showing the raw query) turns a neat demo into an argument.
What's next for Elsewhere
- Multi-city compare: transplant the same you to three cities at once.
- Group transplants: merge a flat's worth of tastes and find the neighborhood that fits everyone.
- Deeper lenses per card price bands, opening hours, transit access and saved, shareable first-week itineraries.
Built With
- ag-ui
- beui
- caddy
- deepseek
- docker
- docker-compose
- fastapi
- leaflet.js
- lucide-icons
- openrouter
- openstreetmap
- python
- qloo-api
- react
- server-sent-events
- shadcn
- tailwind-css
- typescript
- vite
Log in or sign up for Devpost to join the conversation.