Inspiration
Every speaker has had a reference die in the room: the sitcom quote nobody under 30 recognises, the song that means everything to one table and nothing to the next. Writers usually find out live, when it's too late. I wanted a way to find out before you say it, using real data on what a specific audience actually knows and loves rather than a guess.
An AI model on its own is confidently wrong about this. Ask one whether a Whitney Houston reference lands with both 25-year-olds and their parents and it says "yes, she's universal." Qloo's taste data says otherwise. That gap became the project.
What it does
RoomRead is a culture-check agent. Paste a stand-up set, a wedding toast, an ad or a keynote, then pick your audience by age band and/or city.
- Find: it pulls out every cultural reference (shows, artists, films, games, brands) and pins each one to a real Qloo entity.
- Score: each reference gets 🟢 lands, 🟠 mixed or 🔴 misses for that audience, plus a popularity trend.
- Swap: for anything that misses, it proposes a familiar alternative that does the same job in the line, rewrites the line, and re-checks it. You apply swaps one at a time or all at once.
- Receipts: every verdict links to the Qloo calls behind it, so you can see why.
Three modes:
- Check: one audience.
- Bridge: two audiences at once (say, the friends and the parents at a wedding). Each reference is judged on the crowd it serves worse.
- Tour: one script across 2–5 cities, shown as a heatmap, plus a promo blurb for one city that RoomRead checks itself.
A Without Qloo toggle shows what a model alone would have said, side by side, so the difference the data makes is visible.
How I built it
- Next.js 15 + TypeScript front end and API routes. Results stream over Server-Sent Events, so you watch each reference get resolved and scored live.
- Qloo through the official
qlooMCP harness. The agent calls Qloo's tools over MCP (describe,audience_demographics,rank,recommend,entity_tags,trendsand more), and every call is recorded as a receipt. - Qwen (
qwen3.8-max) through an OpenAI-compatible endpoint. It does the language work: extracting references, rewriting lines, and checking whether a swap does the same job. It never decides whether something lands; only Qloo does that. - Scoring. For an age band covering Qloo's age buckets $B$, a reference's age index is
$$I = \frac{1}{|B|}\sum_{b \in B} \text{affinity}_b$$
I don't use fixed cut-offs. $I$ is ranked against six well-known anchor titles of the same type, so a reference counts as green only if it beats most of what that audience demonstrably knows. City scores come from Qloo rank with the city as the location signal. Very niche titles (popularity < 0.5) can never be green, and in Bridge and combined audiences the worse band wins.
- Swap guardrails. A candidate must be at least as familiar as the original, $\text{pop}{\text{swap}} \ge \max(0.95,\ \text{pop}{\text{orig}} - 0.005)$; it must actually improve the band; and it must pass a fit check before it's shown. A retry ladder tries up to three rewrites. If nothing qualifies, RoomRead says "keep the original" instead of forcing a bad swap.
- Reliability. A SQLite cache of Qloo responses, bounded concurrency, per-call timeouts with retries, and SSE keep-alives.
- Deployed as a Docker container on AWS Lightsail (not serverless), with an MCP server of our own (
culture_check) so other agents can call RoomRead as a tool.
Challenges I ran into
- Bad swaps that looked fine. Early versions happily swapped a red reference for an obscure artist that "matched the vibe" but nobody had heard of, or replaced amber with another amber. We added the familiarity bar, the must-improve rule and the fit check, and made "keep the original" a legitimate answer.
- Absolute thresholds didn't work. With fixed cut-offs, almost everything came out red for some audiences. Scoring relative to anchors fixed that.
- Getting the MCP plumbing right. Exact tool argument schemas, natural-language demographics ("adults aged 55-75" maps, "55 and older" doesn't), and demographic signals that need a location before they work.
rankdrops entities. Ask it to rank nine titles and you might get four back, so age scoring moved toaudience_demographics, withrankkept for cities.- Production surprises. MCP timeouts under load, and a load-balancer 504 that surfaced as
Unexpected token '<'. Fixed with timeouts, retries, concurrency limits and stream keep-alives.
What I learned
- The model is great at language and bad at knowing what an audience actually knows. Splitting the work (the model writes, Qloo decides) made the agent trustworthy.
- "No good swap exists, keep it" is a feature, not a failure. Being honest about it builds more trust than always having an answer.
- Receipts matter: showing the data behind each verdict turns a black box into something a writer can argue with.
What's next
- Stronger city-level signal and more audience dimensions.
- Plug-ins for the places scripts live: Google Docs, teleprompters, ad tools.
- Blind reviewer studies to measure how often RoomRead's swaps beat the writer's originals.
Built With
- alibaba-cloud
- amazon-web-services
- aws-lightsail
- dashscope
- docker
- llm
- mcp
- model-context-protocol
- next.js
- node.js
- qloo
- qloo-mcp
- qwen
- react
- server-sent-events
- sqlite
- tailwindcss
- typescript
- zod

Log in or sign up for Devpost to join the conversation.