Inspiration

Virtual models and AI characters are becoming a real channel for brand endorsements. But when a brand asks "which character fits us?", the answer is usually a gut call or a follower count. We run a small roster of original, disclosed AI characters, and we wanted to answer that question with taste data. We also wanted to say "we don't know" when the data doesn't settle it, rather than inventing a winner. We set three rules for the agent: it shows its working, it never pretends a character is a real customer, and it stops before anything is published or paid.

What it does

Brand brief in, ranked AI characters out. Every claim cites its Qloo evidence, and close calls are called close.

A brand (or its agency) fills in a brief: product, category, channels, duration, objective, and a well-known reference brand whose audience resembles theirs. TasteCast then:

  1. Resolves the reference brand with Qloo /search and waits for a person to confirm the right entity. The server enforces this: nothing gets ranked until there is a confirmation.
  2. Ranks three fictional characters (Itzel Robles in Mexico City, Ari Tseng in Seoul, Ingrid Brekkhus in London) with two Qloo signals:
    • a taste index: how strongly the brand's fans like each character's artists, brands, TV and film, as z-scores against a baseline of six everyday brands;
    • audience similarity: a cosine over age brackets and female share.
  3. Checks its own verdict. It returns Clear fit only when the leader is ahead by at least 0.08 and both taste measures (z-score and raw lift) pick the same character. Otherwise it returns a Close call, lists the viable characters and their tradeoffs, and a person casts.
  4. Drafts the campaign package: concept, a 30-second script with timecodes, a shot list, local shoot locations, a soundtrack direction, an image-generation prompt and post-production overlays. It's adapted to the objective and channel (9:16 with a 2-second hook for TikTok/Reels, a 16:9 cut plus a 6-second bumper for YouTube, and so on). Each line says where it came from: Qloo data (with an evidence id), the brief, creative interpretation, or a product rule.
  5. Quotes a licence as machine-readable JSON (tastecast.license_quote.v1). The price is shown and never charged, and both approvals (character operator and brand) stay false until people sign off.

Every endorsement carries #ad · AI-generated character.

The same loop is a remote MCP server, so any agent can drive it: resolve_brand → rank_roster → choose_character → draft_package → quote_license. The tool order is enforced on the server: a candidate_id that didn't come from resolve_brand gets a 409.

How we built it

  • Stack: TypeScript, Hono, and static HTML/JS on Cloudflare Workers. Stored runs, the response cache, the call ledger and per-network limits live in Workers KV. The same createApp() also runs locally on Node.
  • Qloo Insights API drives every decision:
    • /search resolves the brand;
    • /v2/insights demographics gives the audience profile;
    • a reverse-affinity query (signal.interests.entities=<brand>, filter.results.entities=<all characters' seeds>) scores all three characters at once;
    • tag, place and artist queries supply the city-specific package research.
  • Scoring is deterministic and fully shown: fit = 0.6 × taste index + 0.4 × audience. Each part, each heuristic (baseline panel, 0.02 SD floor, ±3σ mapping, 0.08 threshold) and each Qloo call is visible under Technical details. Every evN chip opens the call behind it.
  • The one model step is fenced in. Workers AI (Llama 3.3 70B) optionally rewrites five copy lines. A checker rejects any rewrite that adds a name, brand or number, uses testimonial or "featuring" wording, claims the character uses the product, or drops the disclosure. A rejected line falls back to the template, and the model never writes evidence ids.
  • MCP uses the official @modelcontextprotocol/server SDK. It is stateless and shares one service layer with the web app.
  • Data hygiene: the Qloo key is server-only, clients only see opaque candidate tokens instead of entity IDs, and the public repo holds no Qloo responses. A scan-public pre-push check enforces that. There are 95 tests.

Challenges we ran into

  • Qloo /search never says "unknown". For a brand it doesn't know (e.g. "Loma Coffee Gear"), it returns the nearest names, such as Starbucks. Our first robustness run quietly ranked Starbucks. We now mark exact matches, warn when there are none, and require confirmation before any insights call.
  • Two of our characters looked identical to Qloo. Itzel's and Ari's audience vectors are within 0.003 of each other for youth brands. Only the reverse-affinity taste index separates them (Innisfree: +2.03σ vs +0.39σ). Building the everyday-brand baseline is what made that index meaningful.
  • Tag overlap was weak. Shared tags between brand and character were usually 0–0.1, and city-biased tags overlapped 0–10% with every brand. We kept tags as zero-weight context instead of letting them inflate scores.
  • Our own expectations kept turning out wrong. On 4 of 18 real briefs, Qloo picked a different character than we expected (Fender → Ingrid). We report these instead of re-tuning seeds to get the answer we wanted.
  • More data, fewer verdicts. Adding TV and film seeds raised coverage from 10 to 12 of 12 seeds, but the clear-fit rate dropped from 9 to 8 of 18 briefs. We kept the more complete data.

Accomplishments that we're proud of

  • An agent that can refuse to decide. 10 of 18 real briefs end as close calls. That's the honest state of the signal for three characters whose audiences overlap, and the UI says so instead of manufacturing a winner.
  • Every claim traces back to a Qloo call. No package line carries a Qloo id it doesn't support, and missing brief facts are shown as "to be supplied", never invented.
  • Thresholds set before testing. We wrote down the 0.08 margin and the two-measure agreement rule before the sensitivity and robustness runs, then froze the weights for an 18-brief robustness check.
  • Fast, cheap demos. Four demo briefs and 11 instant reference brands come back instantly from stored results with zero live Qloo calls. Precomputing the 11 instant brands cost just 27 calls, counted by a dry run before spending.
  • Disclosure built in. AI-generated status is labelled everywhere, the licence says "not a testimonial", and the character is never shown using the product.

What we learned

  • Affinity isn't purchase probability. It tells you an audience over-indexes on something, not that anyone will buy, and the product has to say that out loud.
  • A baseline matters more than the raw score. An affinity of 0.3 means nothing until you know what the same affinity looks like among fans of Nike or IKEA.
  • Agent value often comes from guardrails: a fixed tool order, a human checkpoint, a self-check on the verdict, and a hard stop before money or publishing. These made the agent more trustworthy than any added autonomy would have.
  • Letting the LLM write prose is fine as long as it can't touch the facts. Template first, checked rewrite second, evidence ids never from the model.

What's next for TasteCast

  • A blind rating of "with Qloo vs. without Qloo." LLM-only packages for each demo brief are already stored. The comparison panel stays hidden until a real blind rating exists.
  • A bigger roster and more baselines, so close calls become rarer for the right reasons rather than because thresholds were loosened.
  • City-aware scoring. Right now the campaign city adds context only, because city-conditioned affinities sit on a different scale. Calibrating that would let the city affect the ranking.
  • Real licensing workflow: an operator and brand approval flow, a pricing model grounded in market rates (today's tier × channel × duration formula is illustrative), and an export straight into production tools.
  • Curated shoot locations, going beyond the current hand-written filters for other brands' shops, department stores and venues.

Built With

  • cloudflare-workers
  • hono
  • llama-3.3
  • mcp
  • qloo
  • typescript
  • workers-ai
  • workers-kv
Share this project:

Updates

Submission history