Get going · What you get · Providers · Roundtable · Costs · Contribute
Warning
wwxd generates AI impressions of real people. The personas aren't the people they reference, aren't endorsed by them, and will sometimes misrepresent their views. Don't quote outputs. Don't take advice from them. Treat the whole thing as well-researched fan fiction.
Three commands and you're in.
git clone https://github.com/juanfiguera/wwxd.git && cd wwxd
cp .env.example .env.local && pnpm install # add at least one LLM provider key
pnpm dev # → http://localhost:3000A room of any minds you want, arguing your question. Drop Paul Graham, Naval, and Marcus Aurelius into the same conversation and watch them disagree about your next move. The personas speak in turn, react to each other by name, and pass when they have nothing to add — so you get perspectives, not noise. Use it to stress-test your thinking, not to outsource it.
Solo works too — same surface, one persona, when you just want to ask Steve Jobs about your homepage. Save any lineup as a Group ("Board of Directors", "The Stoa") and one-click it from the sidebar.
Two ways to build a persona, mix freely in the same room:
- Grounded — feed wwxd their writing (tweets via Apify, essays from URLs/RSS/sitemap, YouTube transcripts) and replies link back to specific chunks with
↗. You're hearing a synthesis of what they actually wrote. Wears aCITEDpill. - Prior-only — just type a name. Instant. The model generates the persona from its memory. Perfect for historical figures and anyone you can't ingest. Reads "from memory".
Rooms to spin up the moment you've got two or three personas in your sidebar:
- Founders Roundtable — Paul Graham, Naval, Sam Altman: "I have a stable job at a big company and a startup idea I can't shake. What am I missing?"
- AI Lounge — Sam Altman, Andrej Karpathy, Demis Hassabis, Andrew Ng: "Is taste and judgment the most important thing in the age of AI?"
- The Stoa — Marcus Aurelius, Seneca, Naval: "A colleague took credit for my work in front of the team. What now?"
- Design crit — Steve Jobs, Jony Ive, Dieter Rams: paste your homepage hero copy and ask "what would you cut?"
- Devil's advocate — anyone you actually agree with, plus their loudest critic: pitch your idea and let them argue.
After pnpm dev, open http://localhost:3000. The sidebar starts empty with an "Add a persona" card. Two tabs:
- X handle — paste a handle. About a minute for the latest 850 tweets. Needs
APIFY_TOKEN+ an embedding provider. - Name anyone — type a name. Instant. No tokens beyond your LLM provider.
Make one, click their card, ask them something. That's the whole loop.
| URL | What it is | |
|---|---|---|
| Solo | /<slug> |
One on one. |
| Compare | /compare?personas=a,b,c |
Same prompt, parallel columns. See where they differ. |
| Roundtable | /compare?personas=a,b,c&mode=roundtable |
They take turns and react to each other by name. A gate lets a persona pass when they have nothing to add. |
Compare mode looks like this — same prompt, one column per persona, no cross-talk:
Share a conversation as Markdown, plain text, or a versioned .wwxd.json snapshot.
Edit .env.local and point wwxd at whoever you trust. Anthropic, OpenAI, Ollama running locally, anything OpenAI-compatible (OpenRouter, vLLM, LMStudio).
The file is split in two: an API KEYS block at the top, then provider config below. Drop the keys you have, leave the rest blank, and tweak the providers if the defaults don't fit. Common path:
# 1. Drop your keys
ANTHROPIC_API_KEY=sk-ant-... # for chat
OPENAI_API_KEY=sk-... # for embeddings (powers citations)
# 2. Providers (these are the defaults — change if you want)
LLM_PROVIDER=anthropic
EMBEDDING_PROVIDER=openaiAll local with Ollama? Skip the keys above and use:
LLM_PROVIDER=openai-compatible
LLM_BASE_URL=http://localhost:11434/v1
LLM_API_KEY=ollamaFull env reference: .env.example.
Get keys: Anthropic · OpenAI · Apify · Ollama (free, local).
Each turn:
- Gates fire in parallel. A cheap call (
GATE_MODEL) asks every non-first persona "given the conversation so far, do you actually have something to add?" All of them run concurrently via/api/roundtable/gates, so the gate phase takes one call's latency, not N. First speaker bypasses the gate and always speaks. - Speak sequentially. For each persona the gate cleared, retrieve their chunks (grounded) or go from memory (prior-only) and stream via Server-Sent Events. The prompt includes every prior speaker so they can agree, push back, or build on each other by name. Persona replies persist server-side as they stream — close the tab mid-roundtable and the partial reply is preserved.
- Pass. If the gate said NO, surface a
(passed)chip with the reason. No streamText call fires for that persona.
Code: lib/turn-engine.ts, lib/gate.ts, app/api/roundtable/gates/route.ts, and app/api/roundtable/route.ts.
Depends on which providers you pick. Rough ballpark:
| Cost | Notes | |
|---|---|---|
| Tweet ingestion (Apify) | $0.25 to $0.40 per 1k tweets | Latest pull = a few dollars. Deep pulls of a 10-year account = $10 to $30. Skip if you only use prior-only personas. |
| Embeddings | $0.02 per 1M tokens (OpenAI small) | A 5k-tweet corpus is about $0.01. Free if you run them locally. |
| Chat (hosted) | Cents/turn on frontier-tier, fractions on mid-tier | Persona prompt is cached, so each turn mostly pays for retrieval + response. A 4-persona roundtable turn is roughly 4× a solo turn. |
| Chat (local) | Free | Ollama, vLLM, LMStudio. Quality scales with the model you run. |
| Prior-only personas | Zero ingestion cost | Pay only per chat turn. |
Add a persona from the CLI
For batch imports or X archive JSONs:
# Tweets — latest ~850 in X's search window
pnpm fetch-tweets paulg
# Full history — date-windowed, slower, more $$$
pnpm fetch-tweets paulg --deep
# Optional essays — URLs, RSS feed, sitemap, or file
pnpm fetch-essays paulg https://paulgraham.com/founders.html
pnpm fetch-essays paulg --rss https://example.com/feed.xml
# Optional YouTube transcripts
pnpm fetch-youtube lexfridman https://youtu.be/VIDEO_ID
# Embed everything (tweets + essays + transcripts → one index)
pnpm embed-tweets paulg
# /paulg now works.--file on fetch-tweets accepts an X archive JSON if you want to skip Apify entirely.
What's actually happening inside
data flow
─────────
scripts/fetch-tweets.ts → data/<slug>.json corpus
scripts/fetch-essays.ts → appends to data/<slug>.json
scripts/fetch-youtube.ts → appends to data/<slug>.json
scripts/embed-tweets.ts → data/<slug>.embeddings.json vectors
data/wwxd.db conversations, messages, events, groups
data/wwxd-evals.db eval runs (separated from chat data)
ingestion + persona
───────────────────
lib/persona.ts builds the system prompt.
Grounded → voice signature + citation rules.
Prior-only → "use what you know, no citations."
lib/persona-cache.ts caches corpus + BM25 index per persona, mtime-
invalidated. One source of truth for both routes.
lib/disambiguate.ts takes a typed name, returns canonical figure + bio.
Aggressive about spell-correction:
"Marqus Aurelius" → "Marcus Aurelius"
"Elon Mosk" → "Elon Musk"
retrieval + engine
──────────────────
lib/retrieve.ts per-query: embed the user message, run BM25 in
parallel, fuse with RRF, return top-K. Query
embeddings are LRU-cached so a 6-persona round-
table on the same prompt embeds once.
lib/turn-engine.ts the per-persona orchestration. prepareTurn() does
corpus + retrieval + risk + prompt build; runTurn()
adds the streaming wrapper; runGate() runs the
gate decision in isolation for parallel calls.
Same engine serves solo (/api/chat/[username]),
roundtable (/api/roundtable), and the parallel
gate endpoint (/api/roundtable/gates).
lib/db.ts SQLite layer for conversations, messages (with
is_partial flag), participants, groups, and the
structured conversation_events log.
wire format
───────────
/api/roundtable Server-Sent Events: meta, text, gate-passed,
error, done. JSON-encoded payloads so newlines
inside text chunks never collide with framing.
/api/chat/[username] AI SDK UIMessage stream (via toUIMessageStreamResponse).
useChat on the client handles the protocol.
debugging
─────────
/evals/conversations every conversation, ranked by trace data
/evals/conversations/<id> per-turn timeline of gate decisions, retrieval
hits, errors, completion chars
/evals/aggregates health summary + per-persona scorecard +
14-day timeline + heaviest conversations +
retrieval hit-rate
/evals/personas/<username> one persona's complete track record
Tests + evals
pnpm test # 582 unit tests
pnpm eval-persona paulg # voice match score via LLM judge
pnpm eval-discriminate paulg # blind A/B: can a judge tell wwxd from real?A short list, no promises. Open an issue if you want to push one forward.
- More ingestion sources — Substack/Medium without RSS, podcast feeds, book and PDF ingestion.
- Persona share — export a persona's corpus + prompt as a single file others can import in one click.
- Mobile polish — the chat surface is desktop-first; touch, narrow widths, and the share sheet all need love.
- It's a simulation. The disclaimer up top isn't decoration. Keep it visible if you fork.
- No tools, no web access. The chat only knows what's in the corpus (grounded) or what the model knows (prior-only).
- No embeddings = BM25-only, no
↗links. Set up an embedding provider if you want citations. - Prior-only personas never cite. Specific quotes, dates, and numbers may be fabricated. Treat replies as informed extrapolation.
- Everything is local.
data/wwxd.dbholds conversations.data/<slug>.{json,embeddings.json}hold corpora. Nothing leaves your machine unless you share a snapshot.
PRs welcome. Small surface:
- Ingestion —
lib/ingest/,scripts/ - Retrieval —
lib/retrieve.ts,lib/bm25.ts - Persona prompts —
lib/persona.ts,lib/disambiguate.ts - Chat UI —
app/
Run pnpm test before opening a PR. Tests live in lib/__tests__/ and app/components/__tests__/.
MIT, see LICENSE.
If wwxd helps you think, consider giving it a ⭐


