Inspiration
Campus fest committees plan the year's biggest night from last year's list and a WhatsApp poll. The result is predictable: one expensive headliner, three acts that sound the same, a food row nobody visits, and no plan B when the headliner cancels two weeks out.
FestOS treats a lineup as a cultural-fit problem under constraints, not a wishlist. Qloo supplies the taste signal, a deterministic solver does the trade-offs, and the agent shows every step in public.
What it does
Give FestOS a few things your campus actually loves - an artist, a film, a place students hang out at - plus a budget, a city and a per-genre cap. One run returns:
- a headliner, two support acts, a late-night film and three food vendors
- a "why this" line per slot, tied to the signals that surfaced it
- a budget envelope, genre and cuisine spread, and five validation checks
- a full agent trace: resolve -> read -> solve -> validate
Then re-plan in one click: simulate the headliner dropping out, a 25% budget cut, or a stricter genre cap. The new plan is diffed against the old one - kept, switched, new, dropped.
How Qloo is used
All Qloo traffic is server-side on https://hackathon.api.qloo.com with the key in the X-Api-Key header; the browser never sees it.
- Resolve.
GET /search?query=<seed>&types=urn:entity:artist|movie|place. The best match above a 0.48 similarity floor is kept and its Qlooentity_idis used downstream. - Read taste.
GET /v2/insights?filter.type=urn:entity:<kind>&signal.interests.entities=<entity_id>&take=<n>, withfilter.location.query=<city>added for places. Only documented parameters are sent. - Merge and attribute. Every candidate is tagged with the seed(s) that surfaced it; a seed is credited only when its affinity is within 0.25 of the candidate's best score.
- Record. The trace reports every call, cache hit, candidate count and measured solver time. Responses are cached privately for 5 minutes and no Qloo payload is committed to the repo.
Example request (IDs redacted):
GET /v2/insights?filter.type=urn:entity:artist&signal.interests.entities=<artist_id>&take=12
The response provides up to 12 affinity-ranked artists (entity_id, name, tags, affinity), which become solver candidates.
How the lineup is chosen
server/planner.js trims candidate pools to the top 14 artists, 6 films and 9 vendors by measured affinity. It enumerates 3-act combinations under a per-genre cap, 1 film and 3 vendors with distinct cuisines - about 115,000 combinations in a representative run, solved in under 1.5s - and keeps the best feasible lineup by:
0.50 x mean affinity
+ 0.16 x headliner affinity
+ 0.14 x (0.55 x genre spread + 0.45 x cuisine spread)
+ 0.20 x budget fit (targets ~86% of the envelope)
If the budget cannot support the full shape, the solver degrades in a logged order (3 acts + 2 vendors, then 2 acts + 2 vendors, then no film) and every degradation is surfaced as a warning. Budget, genre spread, vendor spread, affinity floor and slot count are re-checked and printed with measured values.
Challenges I ran into
- Silent parameter handling. Undocumented filters appear to be accepted but are ignored, which makes a broken query look successful. I restricted the client to the documented parameter set and verified each response shape.
- One codebase, two runtimes. The API runs on Node (local + Vercel) and on a Deno edge function (Qoder Sites), so routing is path-agnostic via
?op=plan/?op=replanand the client reads env vars on both. - Taste vs budget. Affinity alone produces a lineup that is interesting and unaffordable. Fees are explicitly modelled planning weights - never presented as quotes - and budget fit is its own scored term.
What I learned
Provenance is a product feature. Showing the request, the seed attribution and the measured numbers makes a recommendation auditable and much easier to trust. Server-side keys plus a private cache are not restrictions to work around; they are the right architecture.
Limitations and honesty
- Fees are modelled planning weights, not quotes. Demo entries use synthetic fees; live entries derive a fee from affinity with deterministic jitter.
- Demo mode is explicit. Without
QLOO_API_KEYthe app uses a synthetic catalogue and synthetic affinities. The UI shows a DEMO DATA badge and the API setsmeta.mode = "demo". With the key, both switch to live Qloo. - The agent is deterministic: a tool-using planner (resolve -> read -> solve -> validate -> re-plan), not an LLM wrapper. Every number in the trace is measured.
- Affinity is aggregate, not a prediction about an individual student. Qloo describes what audiences with similar taste tend to like.
- No ticketing or contracting: availability, holds and contracts are out of scope.
- City coverage varies; when Qloo has no local places, the place lookup retries without the address filter and reports
placeScope: broader.
Stack
React 19 + TypeScript + Vite, Express on Node 20+, serverless on Vercel, Deno edge on Qoder Sites. No database, no client-side API keys. Apache-2.0.
Live: https://festos-abhinav-shukla1.vercel.app/
Code: https://github.com/abhinavshukla152005-beep/festos
Built With
- express.js
- node.js
- qloo
- react
- vercel
- vite
Log in or sign up for Devpost to join the conversation.