← Yosef AlnajjarContact
Case study: AI tooling, self-built

Chorus

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.

Chorus, audience picker and persona sociogram

01 · Context & motivation

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.

02 · System design

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.

Architecture: Chorus on AWS
Web App
Mobile App
Partner API
API Gateway
usage plans
Cognito
user pools · authorizer
Lambda
API handlers
Postgres
Aurora Serverless v2 · RLS
SQS Queue
Worker Lambdas
Simulation Engine
AI Model
DynamoDB
reactions
Webhook Lambda
notifies client
CloudWatch
metrics · alarms, watches Lambda & Worker Lambdas

03 · How the Claude API is used

Prompting approach

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.

Evaluation logic

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.

Guardrails

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.

04 · Stack & tooling

ReactTypeScriptViteAPI GatewayCognitoLambdaAurora Serverless v2SQSDynamoDBCloudWatchAnthropic APIZod

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.

05 · What this demonstrates

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.