-
-
City strength: cities with fewer than 15 heatmap cells are flagged as thin data
-
Room setup: type a room, or describe it in one sentence
-
Match confirmation: an ambiguous name blocks ranking until you pick the match
-
Act detail: every number links to its Qloo evidence
-
Slate: Fit, shared tastes, Qloo popularity rank and caveats per act
Inspiration
An independent venue's talent buyer usually judges fit by instinct, and a miss is an empty room. That pain is an inference from vendor writing on booking, not something we measured (marked unverified in the project spec). Qloo's taste data describes audiences in aggregate, which is the right grain for a decision about a room rather than a person. We wanted a decision tool for the buyer, not a consumer recommender: show the ranking, show why, and say plainly where the data is thin or missing.
Aggregate affinity, not a ticket forecast.
What it does
A buyer describes the room (a city and 3 to 6 acts its regular crowd listens to), either by typing it or by writing one sentence that an agent turns into the same fields. They add candidate acts (3 to 12), confirm which Qloo entity each name matched, and get a slate:
- Ranked cards, each with Fit (the mean Qloo tag score shared between the act's audience and the crowd, 0 to 100, computed by code), the top shared tastes and their count, audience standing (a Qloo popularity rank, labelled as that, shown against the crowd's median) and caveats.
- A side-by-side of Fit order against Qloo popularity-rank order, so the buyer sees where the two disagree.
- An act page with the evidence trail: every number links to a redacted summary of the Qloo requests behind it (endpoint, parameters, time fetched, response digest, what it supports). The API key is never stored or shown.
- Nearby-city strength loaded on demand, compared by mean affinity rank and heatmap cell count, with a thin-data flag when fewer than 15 cells back a city.
- An honest empty state for an act Qloo has nothing usable for: no number is shown.
- A read-only share link for a slate, and two pinned sample rooms that open with one click and no input.
Every screen carries the banner "aggregate affinity, not a ticket forecast". The product says nothing about sales, attendance or any individual.
How we built it
- Qloo REST adapter (
packages/qloo, ADR 0012). Endpoints used in the product: search (resolve names to entities), insights compare (shared tags between the crowd and each candidate, and popularity), and the heatmap (city strength). Parameters are built from typed objects, the response shape is asserted on every call (Qloo returns 200 for invalid parameters, so a 200 proves nothing), every call has a timeout, and responses are cached in Postgres. The trending endpoint is wired in the adapter but is flat in the hackathon environment, so no part of the product claims momentum. - Fit formula (ADR 0013): computed in a framework-free core package from the compare response. The LLM never computes or invents a number.
- Agent at the edges (ADR 0009): an LLM (OpenAI, behind a thin provider interface) turns one sentence into a room, in at most 3 steps, and phrases an explanation from fields of the computed slate. A grounding check compares the prose with the slate, and a deterministic template is the fallback. The grounding check accepted recorded model prose at slate scope for two slates (Leipzig and Berlin, one recording each) and at act scope for one Berlin act (one recording); a Leipzig act-scope reply was rejected and the page shows the template there. These are single recordings, not a rate. At a daily budget ceiling the app falls back to the template and to deterministic intake instead of calling the model.
- Fixtures: tests and
DEMO_MODE=fixturesreplay recorded real Qloo responses. There is no synthetic data in the product path. - Stack: TypeScript, Next.js, zod, Drizzle on Neon Postgres, Vitest, Playwright, deployed on Vercel.
- Safety: no personal data is collected or sent to Qloo; the hackathon key is used only server-side against
hackathon.api.qloo.com; entity names from Qloo and from the model are untrusted and rendered as plain text; per-visitor and global daily limits are enforced server-side.
Challenges
- Qloo ignores bad parameters silently. A wrong parameter still returns 200, so shape assertions on every response and typed parameter objects were needed from the first call.
- Heatmap values are rescaled per city, so raw affinity cannot be compared across cities. Cities are compared by mean affinity rank plus cell count, and thin data is flagged (ADR 0013).
- Trending is flat in the hackathon environment, so there is no momentum feature, and audience standing is a popularity rank, not a venue-capacity fit.
- Latency. The first request to a pinned sample room after idle took 3.24 s at the M2 gate. The cause was the function region sitting far from the Neon database; moving functions next to it and removing a duplicate read brought one measurement to 1.72 s.
- Keeping the model honest. Prose is only phrasing of Slate fields and is checked; the checks have known gaps, listed below.
Accomplishments
Measured results, each with its sample and evidence file in the repository (docs/evidence/). None is a pass or fail grade; the samples are small.
- Home-city check: 12 of 15 acts have their home city as the strongest city by mean affinity rank. The strict reading (home city has at least 15 cells and is highest among such cities) is also 12 of 15. Sample: 15 acts, 5 cities (London, Berlin, Lagos, Jakarta, Seoul), 3 continents, 3 acts per city, hand-picked before any heatmap was requested. Misses: BTS, BLACKPINK and IU (Seoul), where Jakarta ranked higher. Caveats: Qloo rescales per city, popularity may influence the result, and a home city is not where an act's listeners live.
- Fit sanity: 11 of 12 same-genre candidates scored above their crowd's off-genre control (electronic 4 of 4, hip-hop 4 of 4, jazz 3 of 4). Sample: 3 crowds, 12 candidates, hand-picked. Margins are small (jazz from -0.2 to +0.8 points, hip-hop +0.4 to +1.3, electronic +1.3 to +4.0), which is why the slate tells the buyer to treat neighbouring ranks as close.
- Agent evals: 106 cases, 1,391 assertions, 0 failed, run offline on recorded traces with no key (
npm run eval:agent). They test the guardrails (input limits, typed Qloo parameters, schema-validated output, names must come from the booker's sentence, the grounding check and the fallback), not that a model will never misbehave. Known gaps, measured by probing: a lowercase invented act name, and a real Slate figure attached to the wrong meaning, are not caught by the grounding check. - Accessibility: an automated sweep of 28 screen states at 360 and 1280 px in light and dark and a keyboard journey test pass; an axe run over 168 captures found no serious or critical issue. These are automated checks; a screen reader pass was not done.
- Timings, with their limits. Sample rooms read a stored slate and make no Qloo call. From one client in Mumbai, production sample-room requests took 1.72 s once after more than 8 minutes idle (two cold samples in all, one location) and 0.22 to 0.54 s warm. A cold slate on production was not measured in the gate runs.
What we learned
- With a data source that returns 200 on bad input, the adapter's response assertions are the real contract.
- Compressed scores need honest presentation: lead with rank, say when values are close, and show the popularity order next to the Fit order rather than hiding the difference.
- Fit is a mean over shared tastes, so an act that shares only a few can score high: in one production run (n=1) the top act shared 8 tastes against 94 for the runner-up. The slate prints the count beside Fit, but there is no minimum-overlap rule yet.
- Letting code own every number and the model only phrase checked fields makes the agent testable offline, and the checks' gaps measurable.
- Reporting small samples as small, with the misses listed, was more useful to us than a headline percentage.
What's next
From the product spec, not built: season-long programming plans, multi-venue circuits, a shared view for artists and agents choosing tour cities, and other live categories such as comedy and sports. Near-term gaps we know of: act-scope model prose has been accepted in one recording only; a cold production slate and a real production rate-limit response are not measured; a screen reader pass is not done; Fit has no minimum-overlap rule.
Testing instructions
No login, account, CAPTCHA or API key is needed. Open https://headliner-ai.vercel.app and choose a sample room (Berlin electronic club or Leipzig indie hall): it opens a stored slate built from real Qloo responses with no input. Open the top act to see shared tastes, nearby-city strength and the Evidence trail. To try your own room, type a city, 3 to 6 crowd acts and 3 to 12 candidate acts, or describe the room in one sentence, then press "Rank these acts". Live Qloo and model calls are capped per visitor and per day; at the cap the page says so. To run it with zero keys: clone the repository, npm ci, then DEMO_MODE=fixtures npm run dev.
Built With
- drizzle
- next.js
- openai
- orm
- playwright
- postgresql
- qloo
- react
- typescript
- vercel
- vitest
- zod
Log in or sign up for Devpost to join the conversation.