PersonaGen

A persona generation API built on probability maps I generated through ~94K LLM calls. LLMs at runtime are slow, biased, and keep producing the same people, so I used them to build the data instead.

Date
2025
Type
Personal Project
Stack
Next.js (App Router)
TypeScript
PostgreSQL
Tailwind
BetterAuth
Nextra
Bun
Elysia
Drizzle ORM
Railway
OpenRouter
PersonaGen landing hero with the headline 'Give your LLM a synthetic persona it hasn't seen before', sub-20ms statistically grounded generation messaging, and a live generated profile preview.

Context and Objective

PersonaGen started as a random idea. It felt like something that should exist as a primitive for AI applications, even though I wasn't sure how useful it would actually be. Programmatically generating realistic demographic personas seemed like an interesting challenge, so I built it.

You could ask an LLM to generate personas at runtime, but they're slow, they keep producing the same people (Sarah Chen appears constantly), and they fence-sit on demographic questions instead of committing to realistic probabilities. I wanted sub-20ms generation with actual statistical grounding; diverse enough to approximate real populations at scale.

  • Next.js (App Router)
  • TypeScript
  • PostgreSQL
  • Tailwind
  • BetterAuth
  • Nextra
  • Bun
  • Elysia
  • Drizzle ORM
  • Railway
  • OpenRouter

Architecture and Delivery

The core bet was that LLMs in 2025 are good enough to generate representational probability data. Not citation-perfect demographics, but distributions that hold up at scale. If I asked for the probability of a 25-year-old in California having a bachelor's degree, I wasn't expecting census-accurate numbers. I was expecting something close enough that when you generate thousands of personas, the outputs feel statistically plausible.

That bet mostly paid off, but getting there took about 94K API calls and ~290 million tokens across 25 different models via OpenRouter. Most couldn't reliably output valid JSON. The ones that could often hedged on sensitive questions or gave implausibly uniform distributions. I tested around 30 prompt variations and regenerated the full corpus three times as models improved.

At runtime there are no LLM calls, just deterministic sampling against pre-built probability maps. Name generation needed its own data pipeline—around 30K names enriched with ethnicity tags and age-band popularity so a 'Muhammad' routes correctly for a British-Pakistani persona, or a 'Siobhan' for an Irish-American one. The generation engine runs on Elysia and Bun for sub-20ms responses. The product surface is Next.js, docs are Nextra.

PersonaGen interface displaying a fully generated synthetic persona with complete demographic profile including age, location, psychology, lifestyle, and physical characteristics.
A generated persona: 77 dimensions from demographics to psychology, each statistically grounded.

Challenges and Trade-offs

The core problem was interdependency. 77 dimensions can't all depend on each other; the data generation scales exponentially. I ended up modeling it like human development: immutable traits first, then life circumstances, then choices. That tiered structure is how the whole system works now.

Even within tiers, each dimension can only realistically depend on 4-12 others before the data generation becomes unmanageable. So I had to pick which dimensions actually matter for each target—age and income affect housing, but eye colour doesn't. Even then, combining those probabilities is surprisingly hard. You can't just multiply them—the compounding wrecks the distribution. I A/B tested about 10-15 different combination approaches across batches of thousands of personas before finding ones that preserved realistic distributions. This was the biggest technical challenge of the project.

Performance took real work to hit sub-20ms. The first version was 200ms+. I replaced naive iteration with pre-indexed lookups, moved to in-memory map caches, and found cheaper sampling methods. Most of the generation path is now just hash lookups and weighted random sampling.

Developer dashboard showing API key management, usage stats, and getting started guide.
Developer dashboard: manage API keys, usage limits, and custom dimension requests.

Outcomes and Takeaways

The main insight was using LLMs to build the data rather than generate at runtime. Probability maps offline, deterministic sampling at runtime. That's how you get speed, reproducibility, and scale.

The raw probability data from LLMs was good enough to capture realistic relationships, but not good enough to use directly. Early versions needed heavy correction rules to patch edge cases. Better combination algorithms removed most of them.

77 dimensions was probably too many. The project would've shipped faster with half that. But the obsession forced me to solve real problems in probability combination and tiered generation that I wouldn't have encountered otherwise.