How many people you could still be.
A lineup, at a police station, is a row of people who all fit the description, and a witness has to pick one out. Every post an account writes adds a line to the description.
[LINEUP ] About 150 people fit everything this account has said
somewhere between 42 and 510
everyone alive 8,200,000,000
lives in a city of 540,000 540,000
gave an age 14,000
gave a job 250
gave a relationship status 150
[COSTS ] What cost the most, and where it was said
lives in a city of 540,000 divides by 15,000
1 post says it, first comments.csv row 3, 2024-01-05
gave a job divides by 57
2 posts say it, first comments.csv row 6, 2023-09-01
That is five sentences out of 1,284 comments: a city, an age, a job said twice and a husband. None of them is personal on its own.
Nobody gives themselves away in one post. They give away a city in one, an age in another, a job and a daughter's birthday in a third, and each of those is harmless by itself and a filter when somebody puts them together.
The arithmetic is old. In 2000, Latanya Sweeney showed that a postcode, a date of birth and a sex are enough to single out 87% of Americans. What changed is the cost of putting the pieces together. In February 2026, a paper by Lermen, Paleka, Swanson, Aerni, Carlini and Tramèr had language model agents read pseudonymous accounts, pull out facts like these, search for who fits them and check the answer. They named two accounts in three, right nine times in ten, for one to four dollars an account.
Deanonymising somebody used to take a person an afternoon. It now takes a script a few seconds, and it can be pointed at everybody.
Almost all of it is on the other side.
Beyond Memorization and the February 2026 paper are the attacks, published as research. Sherlock and Maigret find where else a username has been used, which is the first thing anybody hunting an account does.
On the defending side, Presidio and the tools like it find names, addresses and phone numbers: the things that identify somebody by themselves. None of them looks at the things that only identify somebody in combination, which are the ones people post freely. A 2024 paper by Dou and colleagues trained a model to spot those in a single post and soften them, which is the closest thing to this, and it works one post at a time and never says how exposed the account is as a whole. EFF's Cover Your Tracks does exactly that kind of counting for a browser's fingerprint, in bits. Nothing does it for what you have said.
Lineup is the attacker's arithmetic, done for the person who owns the account, before anybody else does it.
It reads an account the way it was published. Point it at the archive Reddit, X or Mastodon hands over, at Hacker News comments saved from its API, or at a folder of text. Only the account's own words count: a line quoting the comment being answered is somebody else, a retweet or a boost is somebody else's post, and the private parts of an export (messages, the email on the account, the addresses it logged in from) are left alone, because a stranger cannot read them either.
It reads what you say about yourself, in English and in Spanish. Where you live and where you are from, your age, man or woman, your job, your partner, how many children and how old they are, grandchildren, pets, your height, your hand, your degree, your health, your sexuality, the languages you speak. It reads statements the way a person would: "if I were a pilot" is not a pilot, "my 10 year old laptop" is not a child, "I'm 25 years into my career" is not an age, and "I'm a huge Doctor Who fan" is not a doctor.
Spanish gives away one thing English never does. "Estoy cansada", "soy enfermera" and "estoy harto" all agree with whoever is speaking, so somebody who never states their sex has usually stated it forty times without noticing. Lineup counts that grammar as votes, and only believes it when there are three or more and four in five of them agree.
Each fact is applied to the real population it describes. The count starts from the place, spread across ages and sexes the way that country's people are, using the UN's figures for 2025. Every fact after that multiplies each age and sex by the share of people it is true of: the BLS's share of each job held by women, the Census Bureau's share of each age that is married, and so on. Keeping age and sex apart is what makes the count honest about facts that depend on each other. Nearly nine in ten nurses are women, and almost nobody is a grandparent at twenty five.
An age survives time. "I'm 34" written in 2019 is somebody around 41 now, so every age becomes a range of birth years, and two ages said years apart either agree on one or get reported as not adding up. That is one of the ways a made-up persona gets caught.
It reads a clock. Everybody sleeps, and almost nobody posts while they do. The longest quiet stretch in a few hundred timestamps is a night, and the middle of a night is about four in the morning wherever you are. That places an account in a band of timezones without it ever naming a place, and when it has named one, Lineup says if the two disagree.
The answer comes with a range. Every step has a spread, how far off it could be, and there is a cost added for every fact after the first, because facts are not independent and the count treats them as if they were. The range printed is the one that should hold the true number nine times in ten. The calibration below says how often it does.
Every figure is checked against records where the true answer is known.
Against the US census. The Census Bureau publishes a one percent sample of real people, weighted to add up to the whole population. For each person in it who was in work, Lineup was asked how many people in their state share their five years of age, their sex, their job and whether they are married, and the answer was compared with the census's own count.
| state | people compared | median of Lineup against the census | 90% of people between | inside the printed range |
|---|---|---|---|---|
| California | 112,755 | 0.80 | 0.31 and 1.72 | 96% |
| Oregon | 2,312 | 0.60 | 0.27 and 1.18 | 96% |
The count leans low, and part of that is the question itself. A real person is more likely to be in a combination that is commoner than chance would make it, because that is what commoner means, so counting facts as if they were independent comes out low more often than high, and more so with every fact added: age, sex and job alone come to 0.88 in California, and whether somebody is married takes it to 0.80.
Low is the direction it should lean. It tells somebody they are easier to find than they are, never harder. Tell makes the same judgement: being told you are identifiable when you are not costs some care, and being told you are safe when you are not costs the identity.
The same check caught the worst mistake the tables had. A state's population was first estimated from its share of its country's towns, which put California at 58 million against the 39 million the census counts. Regions now carry their counted population in the twenty countries most likely to be run from, and the estimate is only used elsewhere.
Against SynthPAI. SynthPAI is 7,800 Reddit comments written by language models playing 300 invented people, with the facts about each written down. Every fact Lineup read from it was right: thirteen out of thirteen. For any one kind of fact it found one for at most seven of the three hundred people, and that is the honest limit of this program, below.
The census data and SynthPAI cannot live in this repository. The CI downloads both on every push and runs the same checks, so the numbers above cannot quietly stop being true.
- LINEUP is the count, with every step of how it got there.
- COSTS is each fact in order of how much it cut, where it was first said, and how many posts say it. That last number is the one that matters for deleting anything: a fact said in nine posts is still said after one of them goes.
- CLASHES is two things the account said that cannot both be true: two ages that do not add up across the years, or a place that is not in the timezones the account keeps.
- ALSO is what it said that there is no table to count against: a named employer, a named university, a move from one city to another. Any one of these can end the lineup by itself.
- UNREAD is what could not be read, printed first, because a count built on half an export is half a count.
- DRAFT, with
--draft, is what posting something would cost, before it is posted.
The report masks what it found unless you pass --reveal: it says "lives in
a city of 540,000" and not the city, and it never quotes a post. That is so
it can be pasted somewhere to ask for help without carrying the facts it is
warning you about.
lineup reddit-export.ziplineup twitter-archive.ziplineup mastodon-export.ziplineup posts/Reddit, X and Mastodon all let you download your data from their settings,
and Lineup reads the archive as it comes, without unpacking it. A folder of
.txt or .md files is read as one post per file.
Hacker News has no export. This saves your last thousand comments and your profile, and Lineup reads both:
curl -o hn.json "https://hn.algolia.com/api/v1/search_by_date?tags=comment,author_YOURNAME&hitsPerPage=1000"curl -o hn-profile.json "https://hn.algolia.com/api/v1/users/YOURNAME"lineup hn.json hn-profile.jsonTwo accounts given together are counted as one, which is what somebody who linked them would have:
lineup reddit-export.zip hn.jsonAnd before posting something:
lineup --draft reply.txt reddit-export.zipIt says what the draft would cost and what the same thing costs said less exactly: the region instead of the town, the decade instead of the age, the field instead of the job. Leaving a fact out costs nothing. Saying something false instead takes you out of the lineup altogether, as long as it is false the same way every time and points at a crowd rather than at one real person, because the attack only works by trusting what an account says.
--reveal prints the places, the jobs and the words they came from.
Exit code is 1 when the lineup is under a thousand people, 3 when nothing could be read, 0 otherwise.
Windows marks anything that came from the internet, so the first run may bring up a blue "Windows protected your PC" box. Right click the zip, Properties, tick Unblock at the bottom, and then extract it.
It reads statements, not hints. A language model reading the same posts also gets the dialect, the local references, the slang, what somebody knows and what they get wrong, and it gets more than this does. On SynthPAI, whose comments were written to hint rather than say, Lineup found almost nothing, and that dataset was built to test models precisely because they find a lot. So the number this prints is what you have said in so many words, and the real lineup is shorter. The one thing worse than that would be the other way round.
English and Spanish only. Posts in other languages count towards the account's languages, and the report says they were not read.
The job figures are American. The BLS table is the only one that breaks jobs down this finely and by sex, and the same share of each job is used everywhere. A nurse is rarer in some countries than others. The share of people who are married, parents, left-handed or diabetic comes from the US and Europe and is right to within a factor of two in most places this is likely to be run.
Regions are counted in twenty countries. The US, Canada, Mexico, the UK, Ireland, Spain, Portugal, France, Germany, Italy, the Netherlands, Argentina, Chile, Colombia, Peru, Venezuela, Ecuador, Uruguay, Australia and New Zealand. Elsewhere a region's population is estimated from its towns, which overstates the urban ones.
Some things it knows are there and cannot count. A named employer, a school, a move. They go in ALSO and not in the number.
It is not the only thing that links accounts. How you write links them too, and Tell measures that. When you commit links them, and Alibi reads that. Photos, usernames used twice and the people you follow are not here.
A fact it misreads is counted. Sarcasm, fiction, a line of a song.
--reveal shows every sentence a fact came from, so each one can be judged.
Nothing is written, nothing is sent anywhere, no other program is run, and
there is no model in it. Tests read the source and fail the build if a call
that writes a file, an import of net, an import of os/exec or anything
that looks like a model turns up.
That matters more here than in anything else. The thing this is pointed at
is the account somebody is trying to keep separate from their name, and
checking whether it can be traced by sending its posts to somebody else's
model would be the leak itself. A count also has to be something a person
can check: every number in the report is a table in data/, or a share in
the source with where it came from written beside it, and a multiplication.
| table | from | licence |
|---|---|---|
| people by age and sex, 2025 | UN World Population Prospects 2024 | CC BY 3.0 IGO |
| towns of 1,000 people or more, regions | GeoNames | CC BY 4.0 |
| languages by country, names in English and Spanish | Unicode CLDR | Unicode License v3 |
| jobs by sex, 2025 | US Bureau of Labor Statistics, CPS table 11 | public domain |
The calibration also uses the American Community Survey microdata (public domain) and SynthPAI (CC BY-NC-SA 4.0). Neither is in the repository or in the program.
To rebuild the tables from the sources:
sh data/fetch.shgo run ./data/buildThe builder is the only code in the repository that writes a file, and it is not part of the program.
Needs Go and nothing else. No dependencies at all.
go build -o lineup .go test ./...MIT. Do what you like with it.