profile or wanted: com funciona el joc per dins i què mostren 26.452 respostes


profile or wanted va registrar 26.452 respostes de 578 jugadors a les partides lliures, entre el 21 de setembre i l'11 d'octubre de 2026. des del 26 de setembre, el navegador no sap si has encertat fins que has triat: el servidor decideix cada resposta.
és el joc on veus una cara i has de dir si és d'un perfil de linkedin o d'una fitxa de busca i captura de la interpol. l'altre article explica com vaig arreglar les fotos. aquest va del que hi ha al voltant: com es reparteix una partida, d'on surten les cares, com es desplega i què diuen les dades de producció.
què hi ha en producció
next.js amb trpc, prisma i postgres, en un contenidor del meu vps de 2 vcpu i 3,7 gb, que també fa servir coolify i altres apps. les fotos són en un bucket de cloudflare r2.
no cal compte per jugar. un jugador és una cookie, pow_pid, amb l'id signat amb hmac-sha256 i 400 dies de durada. si canvies de navegador ets un altre jugador, així que els jugadors d'aquest article són cookies i no persones. els 578 són les que han respost almenys una cara en una partida lliure. si hi sumes les que només han jugat el repte diari o han cobrat el bonus, són 856 del 19 de setembre a l'11 d'octubre.
el servidor decideix cada resposta
quan comences una partida, el servidor treu les cares a l'atzar, sense repetir-ne cap dins la partida, i escriu cada una a la base de dades amb la seva resposta correcta. al navegador només li arriba una posició i una foto.
model GameRunSlot {
runId String
position Int
personId String
type PersonType
imageVersion Int
guess PersonType?
timedOut Boolean @default(false)
ms Int?
@@id([runId, position])
}
type és la resposta i queda fixada quan es reparteix. guess és null fins que el jugador respon.


la ruta run.guess escriu primer la resposta del jugador i després diu si era correcta. una ronda queda decidida per aquesta primera escriptura, i les peticions repetides reben el veredicte desat. això permet que el client reintenti qualsevol fallada que pugui ser de xarxa, amb una espera de 250 ms que dobla fins a 4 s, amb una mica d'atzar, i que abandoni cada intent als 6 s.
el servidor també fa complir l'ordre: les rondes es resolen una darrere l'altra, una partida de longitud fixa no es pot jugar més enllà del final, i una d'infinita s'acaba amb la primera errada. quan acaba, finish puntua el que guess ha desat i ignora el que el client digui de les seves rondes.
fins al 25 de setembre no era així: les respostes anaven al navegador amb el repartiment, perquè el veredicte sortís sense esperar res. 387 de les 2.203 partides d'aquest article, les del 21 al 25, es van jugar amb aquell sistema. ho vaig canviar el 26 perquè, amb la resposta al navegador, qualsevol altra comprovació del servidor era decorativa. el preu és una petició per ronda. la base de dades és a la mateixa màquina, i el que triga és la xarxa del mòbil. si l'espera passa dels 120 ms, el botó premut mostra una barra de tinta, i als 2,5 s s'ofereix reintentar.
la foto tampoc no pot delatar res. en lloc de la clau de l'emmagatzematge, que contenia l'id de la persona, cada cara es serveix des d'una adreça que és un hmac de l'id.
això no atura tot. un jugador pot buscar una cara, o construir-se una taula de cara i resposta amb els veredictes de moltes partides, perquè el corpus és finit i cada veredicte en revela un parell. el que ho limita és que els punts del rànquing tenen un màxim de 250 al dia.
d'on surten les cares
ara hi ha 522 cares en joc, 248 perfils de linkedin i 274 fitxes de la interpol. n'hi ha 19 més a prova i 8 de retirades, i 1.865 candidates a la base de dades que encara no han entrat.
les cares noves passen per un intake que tria les que a claude sonnet li semblen difícils de classificar, puntuades a cegues sobre fulls de contacte de 12. l'altre article explica la calibració, i una taula, IntakeRecord, recorda cada decisió perquè cap cara es miri dues vegades.
el puntuador encerta en conjunt però no cara per cara, així que cada cara nova comença a prova. entra a una de cada cinc posicions repartides, amb un màxim de tres per partida, i els jugadors la contesten com qualsevol altra. amb 30 respostes es decideixen els casos clars, i amb 60 tots. trial-review compara l'encert de la cara amb el d'una cara en joc típica del mateix tipus, i la manté o la retira.
els 19 casos a prova són tots fitxes, perquè encara no hi ha intake de perfils. un jugador habitual que noti una cara que no ha vist mai pot deduir que és de la interpol, i el codi ho deixa escrit com a pendent.
el repte diari i les tasques programades
el repte diari és el mateix per a tothom: cinc cares, l'última un cap, que és la cara que els jugadors de les partides lliures han errat més els darrers dies, i un gir segons el dia de la setmana. daily-build el prepara a les 03:00 utc, un dia abans.
però el repte també es genera en llegir-lo si falta, amb un bloqueig per data i ON CONFLICT DO NOTHING. si la tasca falla durant dues setmanes, només costa una petició lenta al primer visitant de cada matí. el repte és la pantalla per on passa la retenció, i no ha de dependre d'un cron. des del 20 de setembre daily-build s'ha executat 23 vegades, i les 23 han anat bé.
coolify fa un POST a /api/jobs/<nom> amb un secret compartit. les altres tasques són notice-refresh, que detecta les fitxes que la interpol ja no publica i les marca sense treure-les del joc, daily-reminder, que envia una notificació a les 18:00 utc, stats-rollup i trial-review. cada execució deixa un registre a JobRun amb el que ha fet.
el desplegament
push a main
-> ci: lint, typecheck i tests
-> si passa, deploy.yml compila la imatge del commit que ci ha verificat
-> la puja a ghcr com a latest, main i sha-<commit sencer>
-> avisa el webhook de coolify i espera que acabi
-> coolify descarrega la imatge i reinicia el contenidor
el vps només fa docker pull. compilar next.js allà competiria per la memòria amb l'app que està en marxa, i en una de 2 vcpu i 3,7 gb això es nota. una compilació en fred triga uns 115 s.
vaig provar d'afegir-hi memòria cau de buildkit i va sortir més car: 220 s dels 361 s del pas de compilar per estalviar-ne 50 com a molt, perquè COPY . . va abans de npm run build i la invalida a cada canvi. no n'hi ha.
l'entrada del contenidor executa prisma migrate deploy i surt amb error si falla. per tornar enrere n'hi ha prou de posar a coolify la imatge sha-<commit> d'una de les 5 darreres.
l'única variable obligatòria és DATABASE_URL. les altres fallen tancades: un contenidor sense claus de stripe no ven res, però juga. l'excepció és PLAYER_SIGNING_SECRET, sense la qual no es pot jugar, perquè una clau per defecte permetria a qualsevol fabricar la cookie d'un altre jugador.
com ho mesuro
umami propi, en un servidor meu. el seguiment només envia des del domini de producció, i cada esdeveniment porta l'etiqueta de la versió que el va fer.
els esdeveniments són un mapa tancat a events.ts, en passat i amb propietats de pocs valors, perquè els desglossaments d'umami fan una fila per valor. el detall per cara no hi és, que ja queda a la base de dades.
scripts/kpi-report.mjs llegeix producció amb un rol de només lectura, i la connexió porta a sobre default_transaction_read_only=on. dóna taxes amb el seu recompte, mai sols. una taxa de 5,2% no diu res, una de 5,2% (3/58) sí. amb menys d'uns 100 jugadors actius a la finestra, el que surt és orientatiu.
què mostren les dades
les tres setmanes completes, del 21 de setembre a l'11 d'octubre, diuen això:
- el 72,4% de les respostes són correctes: 19.161 de 26.452. a les fitxes, el 73,9% (9.780 de 13.235), i als perfils, el 71,0% (9.381 de 13.217).
- la resposta mediana triga 1,76 s, i el p90, 6,6 s. les respostes en menys d'un segon són correctes el 71,5% de les vegades (3.006 de 4.203), i les de 3 s o més, el 68,1% (4.998 de 7.334). les cares difícils fan pensar més, així que això no demostra que pensar-hi perjudiqui.
- de 2.203 partides començades, se n'han acabat 1.511, el 68,6%.


el 8 d'octubre hi va haver 529 partides, i l'11, que era diumenge, 8. umami té 1.029 visitants únics, i 608 venen de bing i 604 fan servir edge. tot plegat apunta a ordinadors de la feina o de l'escola. el recompte d'umami és un mínim, perquè el seguiment no va comptar res del 20 al 22 de setembre. el detall de les cerques de bing és a l'altre article.
la majoria no torna: dels 632 jugadors que van començar la primera partida entre el 21 de setembre i el 10 d'octubre, 18 (2,8%) en van fer una altra l'endemà. la línia de base d'abans de tocar res era del 3% al 7%. són poques persones, així que ho llegeixo com a orientatiu.
què canviaria
el primer és que les 387 partides del 21 al 25 de setembre es van jugar quan la resposta anava al navegador. són el 17,6% de les 2.203. no sé si algú va mirar la xarxa, però la resposta hi era, i aquestes partides entren als números d'aquest article.
el segon és fer un intake de perfils, perquè les cares a prova no siguin totes fitxes i una cara desconeguda no delati la resposta.
el tercer és la retenció. un 2,8% l'endemà vol dir que la majoria juga una sola vegada, i és la xifra que cal moure.
el joc és a profileorwanted.com, en català, castellà, anglès i francès.