-
-
The agent asks for the slot budget, above the roster ranked by listener overlap with The Watch
-
A climber's "why it moved" line, and the evidence for the Qloo ranking call it cites
-
The approved insertion order, priced by code, with the close pairs among its shows
-
The launch report, to print or save as PDF
-
The roster's overlap: each show's closest shows by listener overlap, close pairs marked
-
The roster's listeners: each show's lean by age band and gender against the average in Qloo
-
Pitched books ranked against The Big Picture by who listens, beside the pitch's own rank
-
A drafted reply to every pitch, one approved
-
The promo inventory the network's insertion orders commit, show by show
Crossfeed is an agent that ranks a podcast network's own shows, and the public works its pitched guests are promoting, by how much their listeners overlap in Qloo, then drafts the promo plan as an insertion order that only a person can approve.
Live Demo → · Demo Video · Source Code

Try it
- Open crossfeed.m1kuorg.workers.dev and choose Open the sample network. It signs you in as the shared guest in one click, with no passkey, password, or payment. The sample network uses 19 public show names from The Ringer, with arbitrary sample figures; Crossfeed isn't affiliated with The Ringer.
- Go to Launches, open The Watch, and choose Start run, or Run again if a guest ran it already. The agent asks for a slot budget and for shows to leave out: 20 slots and none is a good first answer.
- On the Ranking tab, switch the lens between By who listens and By topic, open a climber to see why it moved, and select any call number, such as C-53, to see that Qloo call's evidence.
- On the Plan tab, edit or approve the insertion order, then export its CSV or open its launch report.
- Go to Inbox, where 40 sample pitches to The Big Picture are already read. Open Rank and choose Rank pitches: Qloo ranks each pitched work by who listens, beside what each pitch says, and the agent drafts a reply to each. This spends one of the guests' shared model runs, and unlike The Watch it has no stored replay.
All guests share the guest network, which is cleared daily at 12:00 UTC, and 10 model runs a day between them. Once those are spent, or if Qloo can't be reached, The Watch replays its stored run, labeled Replayed, and other runs stop and say why. To run it with your own Qloo and Anthropic keys, see Getting started.
Inspiration
The Bill Simmons Podcast is a sports show. Asked to promote The Watch, a show about the week's TV, movies, and music, in the sample run a topic-only plan ranks it 10th. Listener overlap in Qloo ranks it 4th, and Qloo's compare names what the two audiences share: Conversational, Insightful, and Pop Culture.
That's the gap Crossfeed is built on. A network promotes a launch with host-read spots on its own shows, and each of those slots is inventory it could have sold, which is why Crossfeed prices every slot at the network's CPM. A plan drawn by topic puts the launch on the shows that sound like it. Who actually listens is what Qloo can see.
Who it's for
- A network's head of programming, who plans promos with every launch and spends inventory to do it. Crossfeed hands them a ranked, priced plan to approve, with every figure cited, and a report to take to a boss or ad sales.
- Its bookers, who triage pitches every day. Crossfeed ranks each pitched work by the show's listeners, not the publicist's wording, and drafts every reply.
- Its tools: the REST API, SDK, and MCP server put the same moves inside the scripts and assistants a network already uses, without letting them approve anything.
What it does
Crossfeed does two jobs for a podcast network:
- The programmer's launch plan: which of the network's own shows should carry a launch's promos, how many slots each, and what each host should say.
- The booker's inbox: which pitched guests the network's listeners actually want, judged by the public work each guest is promoting, not by the pitch's wording.
Both are the same move: a list the network already owns, its roster or its inbox, ranked in one Qloo call against a show's listeners. Claude Haiku runs the steps, asks the person at each fork, and drafts the text from what Qloo returned. Code prices the plan and enforces the budget. Only a person approves.
Features
- The fit chart: the roster ranked by listener overlap with the launch, in Strong, Good, and Fair bands by thirds, beside a topic-only ranking with no Qloo data. A show that climbs 3 or more places is marked, with a "Why it moved" line citing both rankings.
- Questions at each fork: the agent pauses for the slot budget, shows to leave out, an ambiguous match, or comps that stand in for a launch that hasn't aired.
- Listener check: a finalist whose listeners differ from the launch's by age or gender is flagged Listeners differ and left for the person to judge.
- The insertion order: slots and a host-read angle per finalist, each slot priced in code at downloads per episode × CPM ÷ 1,000. An angle needs shared tags from Qloo; without them the line says so. Approve it, export the CSV, or print the launch report.
- The roster's own map: an Overlap tab of each show's closest shows on the network, with close pairs marked, and a Listeners tab of each show's lean by age and gender.
- The booker's inbox: pitches uploaded as
.eml,.mbox, or text. Claude reads out each guest's public work, Qloo ranks the works in one list per type, and the list switches between By who listens and By the pitch. Every pitch gets a drafted reply to book, decline, or come back to later, which the booker approves, edits, or discards, then copies to send. - Across launches: a Changes tab that compares two runs of a launch, and an Inventory tab of slots committed per show.
- Evidence for every figure: each ranking, tag, and flag cites its Qloo call number, which opens the call's parameters, status, latency, cache hit, and raw scores.
- For a network's tools: a REST API with 30 routes, a TypeScript SDK, an MCP server with 23 tools, and a Claude Code plugin. They can import, run, and draft, but approving an order or a reply only works from a person's page in the app.
How it works
1. The agent asks. The run pauses on a question card until the person answers.

2. Qloo ranks. The roster by listener overlap with The Watch, beside the topic-only plan, with every figure citing its call.

3. A person approves. The insertion order puts 20 slots on the five finalists, priced at the sample $20 CPM, with each host-read angle drawn from the tags Qloo says the audiences share.

4. The case, on paper. The launch report carries the finding, the finalists, the listener check, the close pairs, and the priced plan, with an index of the calls it cites.

5. The inbox. Pitched books, films, TV shows, and podcasts ranked by The Big Picture's listeners, beside a ranking by what each pitch says.

Screenshots
| Screen | What it shows |
|---|---|
| Landing | "Rank by who listens", and one click into the sample network |
| Dashboard | What needs the member, the latest runs, and the day's usage |
| Launches | Every launch, grouped by who's blocked |
| Changes | One run of a launch against another, line by line |
| Inventory | Slots committed per show across the network's orders |
| Overlap | Each show's closest shows on the network, and close pairs |
| Listeners | Each show's lean by age band and gender |
| Inbox | Each pitch's guest, public work, and Qloo match |
| Replies | A drafted reply per pitch, to approve, edit, or discard |
| Developers | The MCP server, the REST API, and API keys |
Built with Qloo
Every ranking, flag, and shared tag in Crossfeed comes from Qloo. The agent calls these through its tools, and one gateway makes every request:
| Qloo call | Parameters | What Crossfeed does with it |
|---|---|---|
GET /search |
query, types (podcast, book, movie, TV show), take=3, or 5 for comps |
Resolves each roster show, the launch, and each pitched work by name. An ambiguous match goes to the person with its candidates; a missed show can be mapped to a comparable one. |
POST /v2/insights |
signal.interests.entities = the launch, or up to 3 comps; filter.results.entities = the roster; feature.explainability=true; take=50 |
Ranks the network's own closed list in one comparison: the fit chart. The same call ranks pitched works, one list per type, and measures the roster's overlap, one call per show matched in Qloo. |
POST /v2/insights |
filter.type=urn:demographics |
Checks the launch and its finalists for age and gender mismatches, and profiles the roster's listeners. |
GET /v2/analysis/compare |
a.signal.interests.entities, b.signal.interests.entities |
The tags two audiences share, which become the "Why it moved" line, the host-read angle, and the episode angle. |
What breaks without Qloo. Without Qloo, an agent can only plan by topic, so Crossfeed runs that plan as its control: Claude ranks the roster from show names and descriptions alone, and the chart shows that rank beside Qloo's, with each show's move between the two. Take Qloo away and there's no fit ranking, no listener check, no shared tags, and so no angles, no ranked inbox, and no overlap. Runs on a network's own data stop and wait for Qloo to answer again.
One door to Qloo. A QlooGateway Durable Object is the only code that calls Qloo:
- at most 3 requests in flight, at least 400 ms apart;
- a 20-second timeout, and up to 2 retries on 429, 502, 503, 504, and timeouts, with jitter;
- a cache keyed by each request's method, path, and parameters;
- a numbered evidence record for every call, which is what the call numbers on screen open;
- a ranking that comes back without scores, or with results outside the list, is an error, not a result.
A network can bring its own Qloo key, which is tested with one search and stored encrypted.
How we built it
Architecture
Made with Archify.
- One Worker runs a Hono app: the pages,
/api/v1, and the MCP server at/mcp. - Durable Objects: a
NetworkAgentper network holds its roster, inbox, overlap, and listener profile; aLaunchAgentper launch holds its runs, ranking, and insertion order; theQlooGatewayholds the Qloo cache and counters. - D1 holds members, passkeys, sessions, networks, API keys, and invites.
- Crons purge expired data hourly, and daily at 12:00 UTC check the Qloo key and reset the guest network.
The agent
- Framework: the Cloudflare Agents SDK, with each session's state synced to the browser over a WebSocket.
- Model:
claude-haiku-5-5, with strict tool schemas and prompt caching. - Tools:
resolve_entities,rank_candidates,ask_human,shared_tags,check_demographics,rank_pitches,draft_insertion_order, anddraft_replies. - Bounds: a run takes at most 12 model turns and 60 Qloo calls. A network starts at most 10 model runs a day, with all guests sharing one count, and all networks together 200.
- People commit:
ask_humanpauses the run until the person answers. Approving an order or a reply has no tool at all, so the model proposes and the person commits.
Tech stack
- Runtime: Cloudflare Workers, Durable Objects, D1, Cron Triggers, the Agents SDK
- Server: Hono with
@hono/zod-openapi, Zod 4,@simplewebauthn/serverfor passkeys,postal-mimefor email files - Pages: React 19, Tailwind CSS 4, shadcn/ui on Base UI, Vite 8
- Model: Anthropic SDK,
claude-haiku-5-5 - Agents and scripts:
@modelcontextprotocol/server, a TypeScript SDK, a Claude Code plugin - Tests: Vitest in the Workers runtime with D1, with Qloo and Anthropic faked by MSW; CI runs Prettier, types, tests, commitlint, and gitleaks on every pull request
Challenges we ran into
- Scores from separate Qloo calls aren't comparable. So the whole roster goes into one call as
filter.results.entities, and the charts show ranks and bands within that one ranking, never raw scores. The cost is a cap of 50 shows, Qloo's largest page; a larger roster is refused rather than split. - A launch that hasn't aired has no listeners yet. Up to 3 comparable shows stand in as the signal, and the compare sends them together, so the shared tags are the union of each comp's.
- Show names aren't IDs. Qloo search is by name, so an ambiguous match goes to the person with its candidates and their popularity, and a missed show can be mapped to a comparable one, whose listeners then set its place.
- An agent shouldn't be able to commit. Approval checks that the request came from a person's page connection, so neither the REST API nor the MCP server can approve, and editing an approved order sends it back to draft.
- A judge needs it to work on the day. The sample launch's last run is stored, so if the guest budget is spent or Qloo fails, it replays, labeled, instead of breaking.
Accomplishments that we're proud of
- What Qloo adds is on screen, not asserted. Every run shows the topic-only plan beside Qloo's ranking. In the run in the screenshots, the Bill Simmons Podcast climbs 6 places over the topic-only plan, and 4 more shows climb 3 or more.
- Every figure cites its call, through to the launch report, which carries an index of the calls it cites.
- A product, not a demo. Passkey accounts, invites, roles, a network's own Qloo key, a REST API, an SDK, an MCP server, and a plugin, all on one Worker, with a one-click guest path for judges.
- Tested at the network boundary. 58 test files drive the Worker's routes, the agents' protocol, the REST API, the SDK, and the MCP server in the Workers runtime.
What we learned
- Rank what the user owns. A network's real decisions are about lists it already has, its roster and its inbox, and
filter.results.entitiesscores exactly those, instead of recommending something new. - Ranks travel; scores don't. Scores from separate calls can't be compared, so ranks and thirds within one call are what can honestly be shown, and the raw scores stay in the evidence.
- A control makes the case. The topic-only plan costs one cheap model call, and it turns "Qloo helps" into a visible difference on every run.
What's next
Known limitations
- The topic plan is one Claude ranking from names and descriptions, a stand-in for how a programmer plans. It's cached per input, so the same roster and launch always get the same plan.
- Fit is listener overlap in Qloo's graph, not a forecast of downloads or conversions.
- A roster holds at most 50 shows.
- The demographic thresholds are starting values, still to be tuned.
- Replies are copied out, never sent, and nothing is pushed to an ad server.
- All guests share one network and 10 model runs a day.
Roadmap
What the PRD left out of the hackathon build: rosters over 50 shows, swap partners from outside the roster, pushing approved orders into ad servers, and measuring a launch's results after its promos run. See open issues for a full list of proposed features (and known issues).
Getting started
Prerequisites
- Bun
- A Qloo API key and an Anthropic API key
Installation
git clone https://github.com/m1kuorg/qloo.git
cd qloo/apps
bun install
Write apps/.dev.vars with QLOO_API_KEY, ANTHROPIC_API_KEY, VISITOR_SALT, QLOO_KEY_SECRET, RP_ID=localhost, and APP_ORIGINS=http://localhost:5128, then:
bunx wrangler d1 migrations apply DB --local
bun run dev -- --port 5128 --strictPort
Run bun run test and bun run typecheck to check it.
License
Apache-2.0. See LICENSE for more information.
Acknowledgments
- Qloo, for the Taste AI API
- Cloudflare Agents SDK
- Archify
- Shields.io
Built With
- anthropic
- claude
- cloudflare-agents
- cloudflare-d1
- cloudflare-workers
- durable-objects
- hono
- mcp
- openapi
- qloo
- react
- shadcn-ui
- tailwindcss
- typescript
- vite
- vitest
- webauthn
- zod
Log in or sign up for Devpost to join the conversation.