Inspiration
When people move to a new city, or move in together, the tools they use compare rent, commute time and safety. None of them answers a simpler question: which part of this city fits how we actually live? And when two people have very different tastes, the usual answer is an average that satisfies nobody. I wanted an agent that finds a neighborhood both people can share, and shows who has to compromise.
What it does
CultureFit takes up to six favorites per person (films, artists, brands, food, games) and ranks neighborhoods in Zurich and Almaty.
- One person or two. With two people, the group fit of an area is the weakest person's score, so a place that one person loves and the other hates cannot win.
- Cost of compromise. Every card shows what each person "gives up" compared with their own best area.
- Evidence, not just a number. Each result shows the venues and tags Qloo returned behind it.
- A live agent. You watch the steps as they happen: matching favorites in Qloo, checking each area, comparing, explaining.
- Refine in plain words. "Quiet", "no bars", "avoid Altstadt" re-run the ranking with those constraints.
- Honest about weak data. If differences are small, or an area has no usable data for someone, the interface says so instead of inventing a score.
How I built it
- Qloo API:
/searchresolves favorites to entities./v2/insightsreturns places around each neighborhood center for each person, using the favorites as taste signals, withfeature.explainabilityenabled. - Scoring: for each person, the average affinity of the five strongest venues in an area is converted to a score from 0 to 100 relative to that person's best area. A minimum spread of 0.08 stops tiny real differences from being stretched across the whole scale. Group fit is the lowest personal score. Scores are relative among the compared areas, not match percentages.
$$ \text{score}=100\left(1-\frac{\text{best}-\text{raw}}{\max(\text{spread},\,0.08)}\right) $$
$$ \text{group fit}=\min\ (\text{scores of all people}) $$
The $0.08$ floor stops tiny real differences from being stretched across the whole 0–100 scale. Scores are relative among the compared areas, not match percentages.
- Backend: FastAPI, async Qloo client with a rate limiter (the hackathon key allows 5 requests per second) and a cache. Progress is streamed to the browser with server-sent events.
- LLM layer: Gemini turns constraints into structured filters and writes short explanations from the returned data only. If the LLM fails, rules and templates keep the app working.
- Frontend: one static page with MapLibre and OpenStreetMap. Deployed on Render, with automated tests and an MIT license.
- I used AI coding assistants during development.
Challenges
- Qloo's heatmap looked tempting but did not work for this. Its affinity tracked popularity closely (rank correlation 0.83 to 0.94 in Zurich), so it mostly showed "where it is busy". Residuals after removing popularity returned venues that did not match the tastes. I switched to scoring neighborhoods from the places Qloo returns around each center.
- Noise in the results. Some tastes returned hotels, chains and utility tags like payment methods. I filter these out.
- Fake precision. My first normalization turned tiny differences into scores from 0 to 100. The minimum spread and a "differences are small" notice fixed that.
- Empty answers. Vague favorites such as "car" returned no usable venues. The app now shows "insufficient data" instead of a made-up score.
- Free-tier limits. LLM quotas and the Qloo rate limit shaped the design: caching, a fallback chain, and an app that works without an LLM.
What I learned
Taste data is only useful if the interface is honest about its limits. Showing venues and tags next to each score, and saying clearly when the signal is weak, makes a result easier to trust than a confident number.
What's next
More cities and custom area boundaries, groups larger than two, and richer explanation graphs when Qloo returns the links between favorites and venues. Today only links that are actually returned are drawn, and none are inferred.
Built With
- css
- fastapi
- gemini-api
- github
- html
- httpx
- javascript
- maplibre-gl
- openstreetmap
- pydantic
- pytest
- python
- qloo-api
- render
- server-sent-events
- uvicorn
Log in or sign up for Devpost to join the conversation.