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
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.
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.
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.