---
title: "profile or wanted: how the game works inside and what 26,452 answers show"
description: "how profile or wanted works inside: the server decides every answer, how faces get in, the deploy, and what 26,452 answers from production show."
date: 2026-10-12
author: "joel taylor pedrós"
lang: en
url: https://joeltaylor.business/blog/profile-or-wanted-how-it-works
translations:
  ca: https://joeltaylor.business/blog/profile-or-wanted-com-funciona
  es: https://joeltaylor.business/blog/profile-or-wanted-como-funciona
image: https://joeltaylor.business/_next/static/media/cover.en.2rn-opvp7ljsa.webp
---

# profile or wanted: how the game works inside and what 26,452 answers show

![under the title the server decides every answer, three cards: 26,452 answers in three weeks, 578 players, and a 2 vcpu vps.](https://joeltaylor.business/_next/static/media/cover.en.2rn-opvp7ljsa.webp)

[profile or wanted](https://profileorwanted.com) logged 26,452 answers from 578 players in free-play games between 21 september and 11 october 2026. since 26 september, the browser doesn't know whether you got it right until you've chosen: the server decides every answer.

it's the game where you see a face and have to say whether it's from a linkedin profile or an interpol wanted notice. [the other post](/blog/profile-or-wanted-linkedin-or-interpol) explains how i fixed the photos. this one is about what's around them: how a game is dealt, where the faces come from, how it's deployed and what the production data says.

## what runs in production

next.js with trpc, prisma and postgres, in a container on my vps with 2 vcpu and 3.7 gb, which also runs coolify and other apps. the photos are in a cloudflare r2 bucket.

you don't need an account to play. a player is a cookie, `pow_pid`, with the id signed with hmac-sha256 and a lifetime of 400 days. if you switch browsers you're a different player, so the players in this post are cookies, not people. the 578 are the ones that answered at least one face in a free-play game. add the ones that only played the daily challenge or claimed the bonus and it's 856, from 19 september to 11 october.

## the server decides every answer

when you start a game, the server draws the faces at random, none repeated within the game, and writes each one to the database with its correct answer. all the browser gets is a position and a photo.

```prisma
model GameRunSlot {
  runId        String
  position     Int
  personId     String
  type         PersonType
  imageVersion Int
  guess        PersonType?
  timedOut     Boolean @default(false)
  ms           Int?

  @@id([runId, position])
}
```

`type` is the answer, fixed when the faces are dealt. `guess` is `null` until the player answers.

![diagram of one round between the browser and the server. the server deals the faces and stores each one's answer in the database. the browser gets only the position and the photo, sends its guess, the server stores it before comparing it and returns the verdict. if the request is repeated, it returns the same verdict.](https://joeltaylor.business/_next/static/media/ronda.en.1q3ze8atjrcod.webp)

_the correct answer leaves the server only after the player's guess has been written. a repeated request returns the verdict that's already there, which is why retrying is safe._

the `run.guess` route writes the player's guess first and then says whether it was right. a round is decided by that first write, and repeated requests get the stored verdict. that lets the client retry any failure that could be the network, waiting 250 ms and doubling up to 4 s with a bit of jitter, and give up on each attempt after 6 s.

the server also enforces the order: rounds are resolved one after another, a fixed-length game can't be played past its end, and an endless one stops at the first miss. when it finishes, `finish` scores what `guess` stored and ignores whatever the client says about its own rounds.

until 25 september it wasn't like that: the answers went to the browser with the deal, so the verdict could appear without waiting for anything. 387 of the 2,203 games in this post, the ones from the 21st to the 25th, were played that way. i changed it on the 26th because, with the answer in the browser, every other check on the server was decorative. the cost is one request per round. the database is on the same machine, and what takes time is the phone's network. if the wait goes past 120 ms, the pressed button shows an ink sweep, and at 2.5 s it offers a retry.

the photo can't give anything away either. instead of the storage key, which contained the person's id, each face is served from an address that's an hmac of the id.

this doesn't stop everything. a player can search for a face, or build a face-to-answer table from the verdicts of many games, because the corpus is finite and every verdict reveals one pair. what limits it is that ranked points have a maximum of 250 a day.

## where the faces come from

there are 522 faces in play now, 248 linkedin profiles and 274 interpol notices. 19 more are on trial and 8 are retired, and 1,865 candidates in the database haven't been let in yet.

new faces go through an intake that picks the ones claude sonnet finds hard to classify, scored blind on contact sheets of 12. [the other post](/blog/profile-or-wanted-linkedin-or-interpol) explains the calibration, and a table, `IntakeRecord`, remembers every decision so no face is looked at twice.

the scorer is right overall but not face by face, so every new face starts on trial. it goes into one in every five positions dealt, with a maximum of three per game, and players answer it like any other. at 30 answers the clear cases are decided, and at 60 all of them are. `trial-review` compares the face's hit rate with a typical live face of the same type, and keeps it or retires it.

all 19 faces on trial are notices, because there's no profile intake yet. a regular player who notices a face they've never seen can deduce it's from interpol, and the code leaves that written down as pending.

## the daily challenge and the scheduled jobs

the daily challenge is the same for everyone: five faces, the last one a boss, which is the face free-play players have missed most in the last few days, and a twist depending on the day of the week. `daily-build` prepares it at 03:00 utc, a day ahead.

but the challenge is also generated on read if it's missing, with a lock per date and `ON CONFLICT DO NOTHING`. if the job fails for two weeks, it only costs the first visitor of each morning one slow request. the challenge is the screen retention runs through, and it shouldn't depend on a cron. since 20 september `daily-build` has run 23 times, and all 23 succeeded.

coolify sends a `POST` to `/api/jobs/<name>` with a shared secret. the other jobs are `notice-refresh`, which detects notices interpol no longer publishes and marks them without taking them out of the game, `daily-reminder`, which sends a notification at 18:00 utc, `stats-rollup` and `trial-review`. every run leaves a record in `JobRun` with what it did.

## the deploy

```
push to main
  -> ci: lint, typecheck and tests
  -> if it passes, deploy.yml builds the image of the commit ci verified
  -> pushes it to ghcr as latest, main and sha-<full commit>
  -> calls the coolify webhook and waits for it to finish
  -> coolify pulls the image and restarts the container
```

the vps only runs `docker pull`. building next.js there would compete for memory with the app that's running, and on a box with 2 vcpu and 3.7 gb you feel it. a cold build takes about 115 s.

i tried adding a buildkit cache and it came out more expensive: 220 s of the 361 s build step to save 50 at most, because `COPY . .` comes before `npm run build` and invalidates it on every change. there isn't one.

the container's entrypoint runs `prisma migrate deploy` and exits with an error if it fails. to roll back, you point coolify at the `sha-<commit>` image of one of the last 5.

the only required variable is `DATABASE_URL`. the rest fail closed: a container without stripe keys sells nothing, but it plays. the exception is `PLAYER_SIGNING_SECRET`, without which nothing is playable, because a default key would let anyone forge another player's cookie.

## how i measure it

my own umami, on a server of mine. tracking only sends from the production domain, and every event carries the tag of the release that made it.

the events are a closed map in `events.ts`, in the past tense and with low-cardinality properties, because umami's breakdowns make one row per value. per-face detail isn't there, since it already lands in the database.

`scripts/kpi-report.mjs` reads production with a read-only role, and the connection carries `default_transaction_read_only=on` on top. it gives rates with their counts, never alone. a rate of `5.2%` says nothing, one of `5.2% (3/58)` does. with fewer than about 100 active players in the window, what comes out is directional.

## what the data shows

the three full weeks, 21 september to 11 october, say this:

- 72.4% of answers are correct: 19,161 of 26,452. on notices it's 73.9% (9,780 of 13,235), and on profiles, 71.0% (9,381 of 13,217).
- the median answer takes 1.76 s, and the p90, 6.6 s. answers in under a second are right 71.5% of the time (3,006 of 4,203), and the ones that take 3 s or more, 68.1% (4,998 of 7,334). hard faces make people think longer, so this doesn't show that thinking hurts.
- of 2,203 games started, 1,511 were finished, 68.6%.

![bar chart of games started per day of the week from 21 september to 11 october 2026. monday 273, tuesday 300, wednesday 553, thursday 748, friday 227, saturday 67 and sunday 35.](https://joeltaylor.business/_next/static/media/setmana.en.1mhdxxttd3ltp.webp)

_the game is played on weekdays: saturday and sunday add up to 102 of the 2,203 games, 4.6%. games started per day of the week, in utc, from 21 september to 11 october 2026, from the profileorwanted.com database._

on 8 october there were 529 games, and on the 11th, a sunday, 8. umami has 1,029 unique visitors, and 608 come from bing and 604 use edge. all of it points to work or school computers. umami's count is a minimum, because tracking didn't count anything from 20 to 22 september. the detail of the bing searches is in [the other post](/blog/profile-or-wanted-linkedin-or-interpol).

most don't come back: of the 632 players who started their first game between 21 september and 10 october, 18 (2.8%) played another the next day. the baseline from before i touched anything was 3% to 7%. it's few people, so i read it as directional.

## what i'd change

the first is that the 387 games from 21 to 25 september were played when the answer went to the browser. that's 17.6% of the 2,203. i don't know if anyone looked at the network, but the answer was there, and those games are in this post's numbers.

the second is building a profile intake, so the faces on trial aren't all notices and an unfamiliar face doesn't give the answer away.

the third is retention. 2.8% the next day means most people play once, and it's the number that needs to move.

the game is at [profileorwanted.com](https://profileorwanted.com), in catalan, spanish, english and french.
