People who fit together
PersonaGen makes up people who could really live in the UK or the US. Each one has 237 facts, such as age, home, job, income, health, habits and views, and the facts fit together. Ask for 1,000 people, or 1,000 private renters in the North West, and they come back as JSON that code or an AI model can use. They're for trying a feature, a price or a message on 1,000 people before building it, checking that an AI assistant treats people of every age, income and background well, pre-testing a survey, and filling a test database with realistic people.
It's an API with docs and a dashboard. Anyone can sign up, create a key and draw 100,000 people a month for free. nxk, another project on this site, uses them as the crowd a coding agent can put questions to.
Why not just ask a model
Ask a language model for a person and you get the same handful of names, jobs and backgrounds again and again. Ask it for 1,000 and you get variations on a few types, never a crowd with the country's real mix of ages, incomes and households. PersonaGen starts from that mix, so each kind of person turns up about as often as they do in the country.
I think models can already answer as a person well enough to point the right way, and nxk's check against published surveys is an early sign of it. PersonaGen is also a bet that they'll get better at it. The better a model plays a person, the more it matters that the person is realistic and the crowd has the right shape.
Rebuilt on published statistics
Early versions were built mostly from probabilities that language models estimated. I measured the last of them against official cross-tables, the published tables that count people by two facts at once. Measured as total variation distance, it was more than 20 points out on 28 UK tables and 27 US tables. The models had their own idea of UK incomes and US debt, and it wasn't the country's. The latest version is rebuilt on published statistics. Across the 131 UK and 122 US tables both versions can be measured on, the average distance fell from 13.7 points to 2.3 in the UK and from 13.5 to 2.0 in the US, about 6 times closer.
Most of that was data work. PersonaGen now draws on 120 datasets by 38 publishers, mostly official bodies such as the ONS, NHS, HMRC and the US Census Bureau, with more than 6 million published figures between them. Wherever an official figure exists, it sets the number.
Most of the ways facts relate to each other are never published, though, and some facts aren't measured at all. No UK table says whether someone lives in a suburb or a city centre, for example. For those, PersonaGen uses estimates from language models, about 1,700 tables per country next to about 330 official ones. The estimates have to agree with the official totals, and they are scored blind against official tables the models never saw. Where they score badly, they count for less. Language models also did labelling that official data doesn't, such as which origins a first name or surname belongs to.
Where the numbers come from
Published statistics set the levels. Language models fill in what nobody publishes.
Published statistics
6.1Mpublished figures, from 120 datasets by 38 publishers
- ONS
- US Census Bureau
- BLS
- CDC
- NHS England
- HMRC
- DWP
- Federal Reserve
- Ofcom
- NISRA
- and 28 more
Tables behind the model, per country
330 official1,700 estimatedEach square is 10 tables
Language models
- 250,000
- model calls
- 60
- models compared
- 29,000
- names labelled
Three things that pull against each other
Every decision trades off three things. Fidelity is a crowd that looks like the country, one fact at a time and in combination. Realism is each person making sense alone. Cost is what it takes to make them. PersonaGen is free to use, so I wanted people quick and cheap enough to make in large numbers. In production it runs on 1 CPU and 2GB of memory.
The obvious fix for an implausible person is to throw them away and draw again. That slows every request, and it biases the population, because the people thrown away aren't random. So I built the realism rules into the model, where they cost nothing per person and don't distort the totals.
Drawn 200,000 at a time, the people match about 250 official tables per country to within about 1 point on the typical table. Most of those are tables the model is fitted to. On the few dozen it's only checked against, the typical table is 1 to 2 points out. There are more than 600 rules, and 400 of them are hard rules, such as no doctor under 23 and no lorry driver without a licence. A release can't ship if a person breaks one. In a blind test on the 38 fields that come from the US census, a language model acting as judge flagged 8.3% of real census respondents as odd and 10.3% of PersonaGen's people, which is too close to tell apart.
Why Rust
The engine builds the model and draws people from it. It began in TypeScript, where a full build took hours on my home server and only a few experiments fitted in a night. I tried Rust on the sampler, the part that draws people, then ported the rest in 3 days. Before switching I had both engines draw 20,000 people per country with no filter, and the output was byte for byte identical. That meant matching how JavaScript rounds floating-point numbers.
Rust, along with building the model once instead of on every request, made the engine more than 10 times faster on UK requests and about 5 to 8 times faster on US ones. Filtered requests are about 4 times faster once a set of filters has been seen. The bigger win was fitting more experiments into a night.
StackRust, Python, TypeScript, TanStack Start, Cloudflare Workers, Railway, PostgreSQL
TypeScript to Rust
The same requests, in milliseconds. Shorter is better.
- One person10× faster
- TypeScript6.3Rust0.6
- A hundred people11× faster
- TypeScript793Rust67
UK, median
Asking for a kind of person
Filters were one of the hardest parts to get right. Facts depend on each other in a chain: age and region shape education, education shapes job, and job shapes income. A filter has to reach back up that chain. Ask for high earners and you should get older, more qualified people in better-paid jobs, not average people with a big salary written in. PersonaGen draws each filtered person from the population that matches the filters, instead of drawing anyone and editing them to fit.
A request can combine up to 10 filters. Before anything is drawn, PersonaGen can say what share of the country a set of filters matches. If no one could match, it refuses and names the filters that conflict, and which one to drop to get a match.
Run like a lab
Getting closer to real people never finishes, so I run PersonaGen like a lab, with Claude Code as the orchestrator. A mining tool looks for where a large draw of people departs from the official tables. Claude reads samples of people and flags the ones that look wrong.
Those findings, and my own ideas, become experiments. Each has a hypothesis and the result it has to show, written down before anything is measured. Coding agents run them in parallel overnight, one branch each, on my Mac and a 4-core home server. When an experiment needs a relationship nobody publishes, the orchestrator can ask the Claude API for an estimate, within a set budget.
Every merge rebuilds both countries and measures them again. An invalid value, a seed that no longer gives the same person or a large regression on an official table stops it. Anything else a change makes worse has to be declared and followed up, and a number the lab has already improved can't slip back without the change saying so. A release can't ship with a broken hard rule, or be worse than an earlier version on any measured gate.
Those gates are what made it safe to give the experiments to coding agents. In 15 days 256 changes merged, and the gates turned back more than 40. PersonaGen keeps improving on its own, inside limits I set. My job is deciding what counts as better and what to give up.
Every change is an experiment
A hypothesis
From what the mining finds or my own idea, with how it will be judged written down before anything is measured.
Its own branch
Run overnight by a coding agent, one of up to a dozen at a time.
The gates
Every field and relationship against the official tables, people who break a hard rule, and the same seed giving the same person.
Merged, or not
Never past a hard blocker. Anything else it makes worse is declared, and a new kind of failure becomes a new check.
287 experiments, 173 promoted
PromotedNot, or not yetIn the order they were opened
256 merges in 15 days