Inspiration

Free streaming TV channels (FAST: Pluto TV, Tubi, Samsung TV Plus, Roku Channel) run 24 hours a day on ads. The money is made in minutes watched, and minutes are lost at the seams: when one show ends and the next doesn't suit the people who were watching, they leave. Programmers order these channels by hand, by gut and by genre. "Crime next to crime" is a guess. What they need to know is whether the people who like this show also like the next one. That is exactly what Qloo measures.

What it does

Lead-In is an audience-flow programming tool for a FAST channel.

  1. Pick a library of shows: one of three demos, or your own (each title is looked up on TMDB for its real episode length and year).
  2. Plan the 24-hour channel. Lead-In asks Qloo how strongly each show's audience likes every other show in the library, and who watches each show. It then searches for the order where every show hands its audience to the next and fits who's watching at that hour, and lays the day on a dial from 06:00 to 06:00.
  3. See where viewers leave. Every handoff is marked green (strong), amber (ok) or red (weak). Dayparts the library serves poorly are outlined, with titles Qloo suggests licensing for that slot.
  4. Try your own order. Move any show earlier or later and every handoff is re-scored instantly.
  5. Ask the Claude agent. Give it a goal ("fix the weakest handoff", "late night is weak, I can license two titles"). It asks Qloo for titles that bridge the two audiences, tests what-ifs on the real schedule, and recommends a change (or none), with the trade-off stated. One click loads its day.
  6. Proof, built in. "vs AI alone" asks the same Claude model to schedule the same library without Qloo, and scores both days on the same scale, plus the listed order and 50 shuffled orders.

On the three demo libraries, Lead-In beat Claude alone every time: audience flow 0.824 vs 0.660 (Classic TV Mix), 0.893 vs 0.769 (Classic Crime & Action), 0.942 vs 0.897 (Cozy Mysteries). Caveat: every day is scored with Qloo's audience overlap and our daypart model, so this measures what Qloo's data adds to scheduling, not real viewer retention.

How we built it

  • Qloo (hackathon API, direct REST) is the core, not decoration:
    • /search resolves each title to a Qloo TV entity (with the year, so Matlock 1986 isn't Matlock 2024).
    • /v2/insights with filter.type=urn:demographics gives each show's aggregate audience skew (age, gender), which we match against who watches in each daypart.
    • /v2/insights with signal.interests.entities = one show and filter.results.entities = the rest of the library gives a directional overlap matrix in one call per show: how much fans of show A like show B.
    • /v2/insights with the whole library as the signal gives titles outside it that share its audience: the licensing suggestions and the agent's bridge titles.
  • Scheduler: a beam search plus seeded local search over 48 half-hour slots, maximising handoff score plus daypart fit under channel rules (every show airs, at most two episodes in a row, a cap on airings).
  • Agent: Claude (Opus) with tool use, in a loop we own: tools to read the plan, find bridge titles on Qloo, find titles for a daypart, and re-plan a what-if. It must quote only numbers the tools returned, and files them under "Qloo says" or "Our read". Spend is capped per run and globally.
  • Hosting: one Cloudflare Worker and a single Durable Object holding the Qloo throttle (5 req/s), a quota reserve, a 7-day cache with stale-if-error fallback, and saved plans, all in SQLite. Keys never leave the server; the browser never calls Qloo.
  • UI: plain TypeScript and DOM, no framework; the dial is hand-built SVG. Light and dark themes, phone to desktop.

Challenges we ran into

  • Qloo's compare endpoint returns shared tags, not an audience number. We built the overlap matrix from insights with filter.results.entities instead, one call per show.
  • Affinities live in a narrow band (about 0.76 for unrelated shows to 0.97 for close neighbours). We map 0.86 → 0 and 0.975 → 1 so differences are visible, and label that mapping "Our read".
  • Quota: 10,000 calls a month for a public demo that judges will hammer. A shared throttle, bounded retries that honour Retry-After, a 500-call reserve and a cache keep a cached plan at zero Qloo calls.
  • Reproducibility: the scheduler originally stopped after 1.5 s of wall time, so a busy server could produce a different day. Demo-size libraries now always finish, and the agent starts from the exact day on screen.

Accomplishments that we're proud of

  • An agent that tests instead of guesses: it re-plans the day for each idea and shows the numbers before recommending.
  • A measurable answer to "would this work the same without Qloo?": no, and the app shows it on any day you plan.
  • Every Qloo call behind a plan is listed in the app, with show names, cached or not.

What we learned

Taste data is most useful in the gaps between things. One show's audience is a demographic; the overlap between two shows is a decision.

What's next

  • Real retention data from a channel partner, to calibrate the overlap-to-retention mapping.
  • Ad-break placement inside slots, and multiple feeds (US vs UK) using Qloo's location signals.
  • Export to channel playout formats.

Request-to-result walkthrough (redacted)

Library: Classic TV Mix (9 shows). Key redacted; entity IDs shortened.

  1. Resolve titles (9 calls): GET /search?query=Columbo&types=urn:entity:tv_show&take=10, keeping the match whose year is 1971 → entity 66969B47….
  2. Audience skew (one per show): GET /v2/insights?filter.type=urn:demographics&signal.interests.entities=66969B47… → age bands and gender skew for Columbo's audience. Qloo says. We score how that skew fits each daypart's typical viewer. Our read.
  3. Overlap matrix (one per show): GET /v2/insights?filter.type=urn:entity:tv_show&signal.interests.entities=<M*A*S*H>&filter.results.entities=<the other 8> → an affinity per target. Example: M*A*S*H → Baywatch raw affinity 0.881, our handoff score 0.185 (weak); Cheers → Frasier 0.974 → 0.992 (strong).
  4. Plan: the solver orders 32 slots; flow 0.824, one weak handoff (M*A*S*H → Baywatch at 11:00). Weak dayparts: Late night (fit 0.32), Early fringe (0.39), Prime (0.44).
  5. Licensing (1 call + demographics for candidates): GET /v2/insights?filter.type=urn:entity:tv_show&signal.interests.entities=<all 9>&take=30 → e.g. Remington Steele (affinity 0.960, match 0.87) for Late night.
  6. Agent: Claude calls find_bridge(M*A*S*H, Baywatch) → Qloo recommends with both as signals → The A-Team, Magnum P.I., The Love Boat… → what_if(+The A-Team) re-plans: flow 0.824 → 0.847, weak handoffs 1 → 0, Prime fit 0.438 → 0.347. It recommends The A-Team and states the trade-off.

Entity and tag choices: TV shows only (urn:entity:tv_show), Qloo's best-covered vertical for this use; no tags are needed because the signal is the shows themselves.

Responsible data handling

  • Only show titles and Qloo entity IDs are sent to Qloo. No names, emails, device IDs or locations of any person.
  • Qloo results are shown as aggregate audience affinities, never as facts about an individual viewer. "Qloo says" and "Our read" are labelled separately throughout.
  • Keys live only in server-side secrets; the browser talks only to our API. Every public route is size-capped and rate-limited; Claude spend is capped.

Known limitations

  • Scores are Qloo's taste overlap and our daypart model, not measured viewer retention.
  • Daypart audience profiles are our assumptions (documented in the app under How it works).
  • Classic, well-known shows are best covered; very new or niche titles may not resolve in Qloo.
  • The agent can only add up to two titles per what-if and three what-ifs per run, to keep cost and latency bounded.

Reproduce

git clone https://github.com/Ramy888/lead-in && cd lead-in
npm install
cp .env.example .env   # QLOO_API_KEY, TMDB_READ_ACCESS_TOKEN, ANTHROPIC_API_KEY (agent optional)
npm test && npm run dev  # http://localhost:8080

Without a Qloo key the app runs on synthetic stand-in data and says so in the header.

Built With

Share this project:

Updates

Submission history