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.

  1. Resolve. GET /search?query=<seed>&types=urn:entity:artist|movie|place. The best match above a 0.48 similarity floor is kept and its Qloo entity_id is used downstream.
  2. Read taste. GET /v2/insights?filter.type=urn:entity:<kind>&signal.interests.entities=<entity_id>&take=<n>, with filter.location.query=<city> added for places. Only documented parameters are sent.
  3. 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.
  4. 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=replan and 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_KEY the app uses a synthetic catalogue and synthetic affinities. The UI shows a DEMO DATA badge and the API sets meta.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

Share this project:

Updates

Submission history