Inspiration
Ask any LLM for music or restaurant recommendations and you get the same answer everyone gets. Not because the model is bad, but because it is recalling what is famous rather than what fits you. It has no taste, only a popularity prior dressed up as a recommendation.
Qloo's taste graph is the obvious fix. But every hackathon project that uses an API claims the API made it better. We wanted to find out whether it actually does, and build the thing that shows you instead of telling you.
What it does
Crossover takes three films you love and gives you music, or three artists and gives you films. Cross-domain taste transfer: the one thing a taste graph can do that a language model genuinely cannot.
The centerpiece is the comparison. The same model answers the same question twice. Once from memory alone. Once as an agent that proposes candidates, checks each one against Qloo, and swaps out the ones that do not fit. You see both lists unlabelled, vote for the one you prefer, and only then find out which was which.
How we built it
FastAPI backend, plain HTML and JS frontend, deployed on Render.
The agent loop is the interesting part. The LLM proposes candidates, our check endpoint resolves each against Qloo's catalog, scores its affinity to your seed entities, and suggests better-fitting alternatives for the weak ones. The agent may only submit picks it has verified. The baseline is the same model, same prompt, no tools.
Measurement, using only fields Qloo actually returns:
- Taste fit: mean affinity of the picks to your inputs
- How mainstream: median popularity percentile
- Variety: mean pairwise Jaccard distance across genre tags
- Agreement: how many entity IDs both lists share
- Blind vote: which list people chose before labels were revealed
Missing data is excluded, never estimated, and every number reports how many picks it was computed from. A pick counts only on a confident match: identical normalized name, and for films a release year within one year.
Challenges we ran into
Most cross-domain pairings do not work, and finding out cost a day. Films to travel returns places where fans live, not places they would want to go. Films to restaurants returns city tourist favorites. Books to travel returns HTTP 400. We tested six pairings and shipped one, because films and music was the only pairing where the results were defensible. The negative results are documented in the repo.
Popularity disagrees with itself. The same entity returns different popularity from search and from insights. Every metric now reads it from insights only, because a number that changes depending on which endpoint you asked is not a measurement.
Fuzzy search always returns something. Qloo's search never comes back empty, so a naive integration would silently score the wrong entity. Hence the strict matching rule. Artists remain imperfect: there is no release year to disambiguate two bands sharing a name, so the most popular match wins.
Rate limits shaped the architecture. Five requests per second, ten thousand per month, about thirteen calls per comparison. Responses are cached and examples are precomputed, because a demo that has exhausted its quota is not a demo.
Accomplishments that we're proud of
The part we are proudest of is what we refused to claim.
The agent wins on taste fit, but that row proves nothing. The agent picks its recommendations by optimizing against Qloo's affinity score, and then we grade it with that same score. Of course it wins. Reporting that as a victory would be circular, so we say so on the page itself.
Same for "verified in Qloo." That figure is guaranteed by the agent's own rule that it may only submit verified picks. It describes the rule, not the quality.
The blind vote is the only measure that neither Qloo nor the LLM controls. It is the one number we would actually defend.
Our baseline also runs an open-weight model on a free tier. A stronger commercial model might recall more from memory, which would narrow the gap. We would rather state that than quietly pick a weak opponent.
Writing down what our own numbers fail to prove killed three metrics that looked impressive and measured nothing. We think the remaining ones are honest, and that is the accomplishment.
What we learned
That the hard part of using a recommendation API is not calling it. It is knowing whether the thing you built is better, or whether you just built something and assumed.
We also learned that negative results are worth keeping. Five failed domain pairings told us more about what a taste graph is actually for than the one that worked.
What's next for Crossover
Persisting the blind vote so the evidence accumulates instead of resetting. Re-testing the failed pairings with location filters and better seed construction. And a proper holdout evaluation with recall@k, which is the rigorous version of what this prototype gestures at.
Built With
- api
- css
- fastapi
- groq
- html
- javascript
- pytest
- python
- qloo
- render
- uvicorn
Log in or sign up for Devpost to join the conversation.