A web tool that simulates how a message lands with 20 distinct AI personas across four audiences (journalists, consumers, investors, policymakers) before you send it.

Most message-testing is either a focus group (slow, expensive, five people) or a gut check (fast, biased, one person: you). Chorus sits in between: describe the message, pick an audience, and get back reactions from twenty distinct fictional people at once, a skeptical tech reporter, a budget-conscious parent, an activist shareholder, a policy researcher, each reacting in character, not as a generic AI sentiment score.
It was built as a concept demo after seeing Artificial Societies' approach to persona-based simulation, to explore how far a single well-constrained API call could get toward something that feels like a room of different people reading your message, rather than one model averaging a response.
Each audience (journalists, consumers, investors, policymakers) is a fixed roster of five personas with a name, role, company, location, generation, political leaning, and a one-line brief on what they're inclined to notice or distrust. Selecting an audience and submitting a message sends one structured-output call to Claude with the full roster embedded in the prompt; Claude returns a sentiment score, a short in-character quote, and a one-word theme for every persona in a single response.
The frontend never talks to Anthropic directly: it calls a /api/simulate endpoint (Express locally, a Vercel function in production) that holds the key server-side, validates the request, and shapes the response. Results render as a force-directed sociogram: nodes colored on a skeptical-to-favorable gradient, with a floating card that surfaces a persona's quote on hover, plus an aggregate consensus gauge and a theme breakdown across the group.
The MVP is deliberately this simple: a single Express/Vercel function was the right amount of infrastructure for a concept demo. The architecture below is how I'd scale it past that (queued simulations, multi-tenant auth, durable storage for reactions) if Chorus moved from demo to product.
One prompt, one call, per simulation: the full persona roster (name, role, location, generation, political leaning, industry, and a one-line brief) is embedded directly in the prompt, with an explicit instruction to stay varied and not let personas converge on agreement. The untrusted message being tested is fenced off in the prompt, and the model is told explicitly to treat it as content to react to, never as instructions to follow.
Output is constrained with the Anthropic SDK's structured-output support: a Zod schema of id, sentiment, quote, keyword per persona, so there's no markdown-fence stripping or manual JSON parsing to get wrong. If a response is missing a persona or a field, the backend falls back to positional matching and safe defaults rather than failing the whole run.
A refusal check surfaces model declines as a clean error instead of a blank result. Anthropic API errors (rate limits, auth failures, provider errors) are caught and translated server-side, so no stack traces or key material ever reach the client. Two independent in-memory rate limiters, per-IP and a global daily cap, sit in front of the endpoint as a speed bump against runaway usage, with a hard spend limit on the Anthropic workspace as the real backstop.
Zod defines the response schema once and drives both the structured-output contract with Claude and the type the frontend consumes: the same shape validated on the way out of the model is the shape rendered in the UI, with no translation layer in between.
Zod defines the response schema once and drives both the structured-output contract with Claude and the type the frontend consumes: the same shape validated on the way out of the model is the shape rendered in the UI, with no translation layer in between.
The instinct here is the same one that shows up across this work: treat an LLM call as a component with a typed contract, not a chatbot bolted onto a UI. The interesting decisions are what to hold fixed: the persona roster, the output schema, the prompt's instruction to stay varied, versus what to leave to the model, and building the trust boundary (message in, structured JSON out, server-side key) the same way you'd design any other external API integration.