Inspiration

Independent podcasters sell host-read ads with almost no audience research. Two shows in the same category end up pitching the same brands, because the only signal they have is the topic label. A host-read ad is paid for taste. The topic is a weak proxy for it.

A language model can already list brands for a category. That list is a guess. What a host cannot get from the model is a taste card for the audience, a brand shortlist scored from that graph, and a pitch where every sentence points at a source. That is the report overindex builds.

What it does

One podcast name becomes a Sponsor Report.

Open a featured report first. Four cached reports are dated 2026-10-21: This Week in Startups, SmartLess, New Heights with Jason & Travis Kelce, and Gastropod. Opening one does not start a live report and does not call Qloo.

A report shows:

  • An audience taste card from Qloo: tags, artists, TV shows, movies, books, and demographics. Each block has its own status. The figures are aggregate taste-graph values.
  • A fit quadrant. Proven means the brand is on the language-model list and above the fit cutoff. Hidden means it is above the cutoff and not on that list. Category-only means it is on the list and below the cutoff. Brands low on both axes stay off the chart. A cell can be empty. The report leaves it empty.
  • Two lanes. Ready buyers are brands that appear in a public podcast-ad ground truth. Greenfield is the rest.
  • A Without Qloo toggle. It shows the language-model list, and every row is labeled model list, no taste graph. That toggle is the comparison. The pre-registered test did not clear its bar. The figures are in Limitations and on the Method page.
  • A brand drawer. Draft pitch is built from evidence already on the report. It does not call a model. Each claim is labeled Qloo or input Anda. There is no send-email button.

Fit on the report equals the show's affinity percentile. Alpha is 1, so the lift weight is 0. The product name is overindex. The number is a percentile inside the candidate set, not an index against the general population. The quadrant cutoff on a report is 0.7. That cutoff was not an evaluation result.

A live report is capped at 3 starts per address per calendar day in WIB. A failed start still counts.

How we built it

Next.js and TypeScript, on one Compute Engine VM. Qloo is called only through @qloo/qloo-harness 0.1.26.

The harness exposes the calls as MCP workflows: qloo_describe, qloo_entity_tags, qloo_audience_demographics, and qloo_recommend. Scoring and popularity bands use qloo api insights, because the recommend workflow does not expose those parameters. A fixed orchestrator calls that sequence. The model does not choose the tools. It is used in two structured steps: narrowing a semantic show match, and writing the Without Qloo list. The list step receives the show name and a description. It does not receive affinity scores.

The server refuses to call unless the harness base URL is https://hackathon.api.qloo.com. The key stays in the server environment. The browser never receives it. This app does not call Qloo with its own fetch.

A live report does this:

  1. qloo_describe resolves the podcast. A confident match continues. needs_input shows every candidate and waits. An empty result stays empty. The report does not invent a proxy show.
  2. The taste card calls qloo_entity_tags, qloo_audience_demographics, and qloo_recommend for artist, TV show, movie, and book.
  3. Brand discovery uses qloo api insights with two popularity bands.
  4. Names from the model list, the ready-buyer catalog, and any sponsors the user typed are resolved with qloo_describe, inside a call budget. Qloo then scores that shared candidate set with filter.results.entities.
  5. The same insights call, against a fixed basket of eight podcasts, produces the baseline. The basket is published on the Method page.
  6. check_claims drops numbers that are not in the evidence, causal sentences, and claims about an individual.

The hard cap is 40 Qloo calls. The target is 25. Past the plan, the candidate section is marked partial and the run stops adding calls.

One public live run, 2026-10-22, query Crime Junkie. No response body, entity id, brand name, or key:

POST /api/resolve  status ok, 569 ms
POST /api/report   HTTP 202, done 43.7 s, mode direct
taste card ok, candidates partial, Without Qloo ok
budget.used 38 of cap 40, trace events 88, tool results 40
qloo_describe 28, api:insights 6, qloo_recommend 4
qloo_entity_tags 1, qloo_audience_demographics 1
qloo_rank not called

Forty tool results were recorded and budget.used is 38, so two trace results did not add to the budget.

Setup from a clean checkout, including the required hackathon base URL, is in the README.

Challenges we ran into

qloo_rank returned 429 during the spike. Scoring moved to qloo api insights with filter.results.entities. Those scores were checked for scale: identical across calls and across candidate sets.

Brand names often fail to resolve. On the spike, strict resolution was 7 of 29. On the locked test split, 13 of 176 ground-truth brands resolved. Unresolved names stay unresolved. The report does not substitute a nearby brand.

A shared-TV bridge between the show audience and a brand was 0 of 5 in the spike. On a cached featured report that bridge is skipped when it was not saved. Opening a featured brand does not call Qloo. Trend lookup is not called.

Each harness call costs several seconds. On an e2-micro, the Crime Junkie report used 38 calls and finished in 43.7 seconds, with candidates marked partial. The local browser timing from 2026-10-20 was 89.9 seconds for 35 calls. The public number is the one above.

The hackathon key is rejected by the harness default host. The base URL has to be https://hackathon.api.qloo.com, or every call fails authentication.

Accomplishments that we're proud of

A judge can open a finished report, read a taste card, open the model list beside it, and read a pitch where each sentence has a source. Featured reports are cached, so that path does not spend the live quota. Empty quadrants stay empty.

The evaluation was frozen before the test split was measured. It was measured once, on 2026-10-21 at 03:17 WIB. The script refuses a second run. The result is on the public Method page, including the metric that moved against the Qloo list. Ground truth is not in the public image. Qloo response bodies are not in the repository.

What we learned

The test asks one question. Given the same candidate brands, does a Qloo score recover known sponsors better than a language-model ranking, and is the list more specific to the show?

It did not clear that bar. recall@20 is lower for the Qloo list, and that confidence interval sits entirely below zero. Jaccard does not. The rule treats the list as better only when both move. On nDCG@10 there is no difference against the language-model ranking. With lambda 0 the hybrid score is the popularity percentile, so that comparison matched popularity.

The taste card, the sourced pitch, and the side-by-side model list do not depend on winning that test. A high fit score is a different fact from "this brand bought an ad." The model may already know older sponsors. The graph may already reflect podcast advertising.

Limitations

One test run, 2026-10-21 03:17 WIB. It was not repeated. Locked settings: alpha 1, lambda 0, prompt A.1a, baseline basket-8, no quadrant threshold.

No difference was detected between overindex and the language-model list (n = 15). The Jaccard difference, overindex minus the language-model list, is -0.0094 (95% CI -0.0223 to 0.0048). That interval does not lie entirely below zero. The recall@20 difference is -0.1064 (95% CI -0.2193 to -0.0255). That interval lies entirely below zero. A tighter list is not treated as better unless both conditions hold.

No difference was detected on nDCG@10 between hybrid and the language-model ranking (n = 9). The paired difference, hybrid minus the language-model ranking, is 0.0492 (95% CI -0.1182 to 0.2158). With lambda 0, the hybrid score is the popularity percentile, and hybrid nDCG@10 matched popularity at 0.7659 (95% CI 0.5131 to 0.9636).

The test split has 20 shows. 15 resolved. Q2 measured 15. Q1 measured 9. Ground-truth brands resolved 13 of 176. No blind preference test has been run. This project does not claim that people prefer it, and it does not claim to beat a language-model list.

The taste card is an aggregate from Qloo's taste graph. It is not a survey of that show's listeners. There is no causal claim and no claim about an individual. A brand name can resolve to the wrong entity. The public demo IP is ephemeral.

What's next for overindex-podcast

Not in this build: a brand-side mode, live-show cities, and a commercial Qloo license. Any next measurement needs a new pre-registered test. This split stays closed.

Built With

Share this project:

Updates

Submission history