Inspiration
A "taste" recommendation engine usually means one of two failures: the model invents entities that do not exist, or the recommendations are so generic they could apply to anyone. Cultural affinity is the part that is genuinely hard to fake, which is why grounding it in a real taste graph rather than a prompt is the interesting problem.
What it does
The agent resolves a free-text taste description into Qloo entities, then asks the Insights API for cross-domain affinity and merges the answers into one ranked list.
- Seed resolution -
GET /search?query=...&types=urn:entity:artistturns "shoegaze and slow cinema" into real entity ids. - Cross-domain affinity -
GET /v2/insights?filter.type=urn:entity:movie&signal.interests.entities=<id>returns ranked entities with the affinity the API computed. - Merge and trace - results are merged by
domain:id, keeping the higher affinity, and every call is recorded with its status, duration and entity count.
Nothing is scored locally. If the API returns nothing, the agent says so instead of filling the gap.
How we built it
src/qloo-client.ts- the HTTP boundary:X-Api-KeyandAuthorization: Bearerboth sent, 12 sAbortControllertimeout, retry on 429/5xx honouringRetry-Afterwith jittered backoff, and a parser that reads the documented envelope (entities/results/data) while keeping the raw record on every entity.src/agent-pipeline.ts- one seed resolution, one insights call per (domain x seed), merge by affinity, trace step per call.src/mcp-server.ts- MCP tools (qloo_search_entities,qloo_insights,taste_recommend) over the same pipeline.scripts/contract-server.mjs- a transport fixture implementing the documented response shapes over real sockets, so auth, retry, timeout and parsing are testable without a key.
Challenges we ran into
The first version of this project answered from a hand-written CURATED_TASTE_GRAPH constant with fictional vibeVectors fields and a placeholder API key. It could not return a 401, could not time out, and could not be wrong - which meant it could not be evaluated either. Deleting it and putting a real HTTP client in its place was the whole rewrite.
The second problem was the shape: /v2/insights is a GET with query string parameters, not a POST body, and a hackathon key only works against hackathon.api.qloo.com - the staging and production hosts return 401 even with a valid key. Both are documented, both are easy to get wrong.
Accomplishments that we are proud of
- 7 tests over real HTTP: missing key, 401 without retry, 429 retried twice then succeeding, envelope parsing and affinity sorting, 404 propagation, end-to-end trace, and an empty-search case that must not invent entities.
- A recorded run: 4 HTTP requests, 8 recommendations across three domains, p50 57 ms with a 35 ms injected server delay.
- The client refuses to answer without a key rather than falling back to local data.
What we learned
An integration that cannot fail is not an integration. The most useful thing in this project is the QlooAuthError path and the retry trace, not the recommendation list.
What is live right now
The hackathon key was issued and the recorded run is live against hackathon.api.qloo.com. Running it broke three assumptions that a fixture could never expose:
/v2/insightsnests its payload:{success, results: {entities: [...]}}, not a top-level array.- Affinity is per entity inside
query.affinity, not at the top level. Until that was fixed every score was null and ranking fell back to alphabetical order. /searchmatches names, not concepts: "shoegaze and slow cinema" returned acts literally called Cinema, and the recommendations that followed were an airport and a museum./v2/tagswithfilter.queryunderstands genre vocabulary, so tags are now resolved first and passed assignal.interests.tagsnext to any entity signals./v2/tagsalso rejects a barequery=with 400.
The live run returned 12 recommendations across artist, movie and place in 5 requests, p50 4963 ms, with one 429 retried under the configured policy. The artist bucket came back as the Japanese shoegaze scene (東京酒吐座, Burrrn, Shojoskip), the movie bucket as Sion Sono and Hirokazu Kore-eda, the place bucket as live-music rooms. Nothing in the repository knows those names.
What is next
Calibrate affinity across domains (a movie score and a place score are not currently comparable even though the UI sorts them together), add a labelled set so relevance can be measured instead of asserted, and expose the MCP tools to a real client session.
Built With
- ai-agent
- api-integration
- mcp
- node.js
- qloo
- taste-graph
- typescript