---
title: "profile or wanted: cómo funciona el juego por dentro y qué muestran 26.452 respuestas"
description: "cómo funciona profile or wanted por dentro: el servidor decide cada respuesta, cómo entran las caras, el despliegue y qué muestran 26.452 respuestas de producción."
date: 2026-10-12
author: "joel taylor pedrós"
lang: es
url: https://joeltaylor.business/blog/profile-or-wanted-como-funciona
translations:
  ca: https://joeltaylor.business/blog/profile-or-wanted-com-funciona
  en: https://joeltaylor.business/blog/profile-or-wanted-how-it-works
image: https://joeltaylor.business/_next/static/media/cover.es.14gam983mamda.webp
---

# profile or wanted: cómo funciona el juego por dentro y qué muestran 26.452 respuestas

![bajo el título el servidor decide cada respuesta, tres tarjetas: 26.452 respuestas en tres semanas, 578 jugadores y un vps de 2 vcpu.](https://joeltaylor.business/_next/static/media/cover.es.14gam983mamda.webp)

[profile or wanted](https://profileorwanted.com) registró 26.452 respuestas de 578 jugadores en las partidas libres, entre el 21 de septiembre y el 11 de octubre de 2026. desde el 26 de septiembre, el navegador no sabe si has acertado hasta que has elegido: el servidor decide cada respuesta.

es el juego en el que ves una cara y tienes que decir si es de un perfil de linkedin o de una ficha de búsqueda y captura de la interpol. [el otro artículo](/blog/profile-or-wanted-juego-linkedin-interpol) explica cómo arreglé las fotos. este va de lo que hay alrededor: cómo se reparte una partida, de dónde salen las caras, cómo se despliega y qué dicen los datos de producción.

## qué hay en producción

next.js con trpc, prisma y postgres, en un contenedor de mi vps de 2 vcpu y 3,7 gb, que también usa coolify y otras apps. las fotos están en un bucket de cloudflare r2.

no hace falta cuenta para jugar. un jugador es una cookie, `pow_pid`, con el id firmado con hmac-sha256 y 400 días de duración. si cambias de navegador eres otro jugador, así que los jugadores de este artículo son cookies y no personas. los 578 son las que han respondido al menos una cara en una partida libre. si sumas las que solo han jugado el reto diario o han cobrado el bonus, son 856 del 19 de septiembre al 11 de octubre.

## el servidor decide cada respuesta

cuando empiezas una partida, el servidor saca las caras al azar, sin repetir ninguna dentro de la partida, y escribe cada una en la base de datos con su respuesta correcta. al navegador solo le llega una posición y una foto.

```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` es la respuesta y queda fijada al repartir. `guess` es `null` hasta que el jugador responde.

![diagrama de una ronda entre el navegador y el servidor. el servidor reparte las caras y guarda la respuesta de cada una en la base de datos. el navegador recibe solo la posición y la foto, envía su respuesta, el servidor la guarda antes de compararla y devuelve el veredicto. si se repite la petición, devuelve el mismo veredicto.](https://joeltaylor.business/_next/static/media/ronda.es.0gux5atyah24m.webp)

_la respuesta correcta solo sale del servidor después de que se haya escrito la del jugador. una petición repetida devuelve el veredicto que ya había, y por eso se puede reintentar sin riesgo._

la ruta `run.guess` escribe primero la respuesta del jugador y después dice si era correcta. una ronda queda decidida por esa primera escritura, y las peticiones repetidas reciben el veredicto guardado. eso permite que el cliente reintente cualquier fallo que pueda ser de red, con una espera de 250 ms que se duplica hasta 4 s, con algo de azar, y que abandone cada intento a los 6 s.

el servidor también hace cumplir el orden: las rondas se resuelven una tras otra, una partida de longitud fija no se puede jugar más allá del final, y una infinita termina con el primer fallo. cuando acaba, `finish` puntúa lo que `guess` ha guardado e ignora lo que el cliente diga de sus rondas.

hasta el 25 de septiembre no era así: las respuestas iban al navegador con el reparto, para que el veredicto saliera sin esperar nada. 387 de las 2.203 partidas de este artículo, las del 21 al 25, se jugaron con ese sistema. lo cambié el 26 porque, con la respuesta en el navegador, cualquier otra comprobación del servidor era decorativa. el precio es una petición por ronda. la base de datos está en la misma máquina, y lo que tarda es la red del móvil. si la espera pasa de 120 ms, el botón pulsado muestra una barra de tinta, y a los 2,5 s se ofrece reintentar.

la foto tampoco puede delatar nada. en lugar de la clave del almacenamiento, que contenía el id de la persona, cada cara se sirve desde una dirección que es un hmac del id.

esto no lo para todo. un jugador puede buscar una cara, o construirse una tabla de cara y respuesta con los veredictos de muchas partidas, porque el corpus es finito y cada veredicto revela un par. lo que lo limita es que los puntos del ranking tienen un máximo de 250 al día.

## de dónde salen las caras

ahora hay 522 caras en juego, 248 perfiles de linkedin y 274 fichas de la interpol. hay 19 más a prueba y 8 retiradas, y 1.865 candidatas en la base de datos que todavía no han entrado.

las caras nuevas pasan por un intake que elige las que a claude sonnet le parecen difíciles de clasificar, puntuadas a ciegas sobre hojas de contacto de 12. [el otro artículo](/blog/profile-or-wanted-juego-linkedin-interpol) explica la calibración, y una tabla, `IntakeRecord`, recuerda cada decisión para que ninguna cara se mire dos veces.

el puntuador acierta en conjunto pero no cara por cara, así que cada cara nueva empieza a prueba. entra en una de cada cinco posiciones repartidas, con un máximo de tres por partida, y los jugadores la responden como cualquier otra. con 30 respuestas se deciden los casos claros, y con 60 todos. `trial-review` compara el acierto de la cara con el de una cara en juego típica del mismo tipo, y la mantiene o la retira.

los 19 casos a prueba son todos fichas, porque todavía no hay intake de perfiles. un jugador habitual que note una cara que nunca ha visto puede deducir que es de la interpol, y el código lo deja escrito como pendiente.

## el reto diario y las tareas programadas

el reto diario es el mismo para todos: cinco caras, la última un jefe, que es la cara que los jugadores de las partidas libres han fallado más en los últimos días, y un giro según el día de la semana. `daily-build` lo prepara a las 03:00 utc, un día antes.

pero el reto también se genera al leerlo si falta, con un bloqueo por fecha y `ON CONFLICT DO NOTHING`. si la tarea falla durante dos semanas, solo cuesta una petición lenta al primer visitante de cada mañana. el reto es la pantalla por la que pasa la retención, y no debe depender de un cron. desde el 20 de septiembre `daily-build` se ha ejecutado 23 veces, y las 23 han ido bien.

coolify hace un `POST` a `/api/jobs/<nombre>` con un secreto compartido. las otras tareas son `notice-refresh`, que detecta las fichas que la interpol ya no publica y las marca sin sacarlas del juego, `daily-reminder`, que envía una notificación a las 18:00 utc, `stats-rollup` y `trial-review`. cada ejecución deja un registro en `JobRun` con lo que ha hecho.

## el despliegue

```
push a main
  -> ci: lint, typecheck y tests
  -> si pasa, deploy.yml compila la imagen del commit que ci ha verificado
  -> la sube a ghcr como latest, main y sha-<commit entero>
  -> avisa al webhook de coolify y espera a que acabe
  -> coolify descarga la imagen y reinicia el contenedor
```

el vps solo hace `docker pull`. compilar next.js allí competiría por la memoria con la app que está en marcha, y en uno de 2 vcpu y 3,7 gb se nota. una compilación en frío tarda unos 115 s.

probé a añadir caché de buildkit y salió más caro: 220 s de los 361 s del paso de compilar para ahorrar 50 como mucho, porque `COPY . .` va antes de `npm run build` y la invalida en cada cambio. no hay.

la entrada del contenedor ejecuta `prisma migrate deploy` y sale con error si falla. para volver atrás basta con poner en coolify la imagen `sha-<commit>` de una de las 5 últimas.

la única variable obligatoria es `DATABASE_URL`. las demás fallan cerradas: un contenedor sin claves de stripe no vende nada, pero juega. la excepción es `PLAYER_SIGNING_SECRET`, sin la cual no se puede jugar, porque una clave por defecto permitiría a cualquiera fabricar la cookie de otro jugador.

## cómo lo mido

umami propio, en un servidor mío. el seguimiento solo envía desde el dominio de producción, y cada evento lleva la etiqueta de la versión que lo hizo.

los eventos son un mapa cerrado en `events.ts`, en pasado y con propiedades de pocos valores, porque los desgloses de umami hacen una fila por valor. el detalle por cara no está, que ya queda en la base de datos.

`scripts/kpi-report.mjs` lee producción con un rol de solo lectura, y la conexión lleva encima `default_transaction_read_only=on`. da tasas con su recuento, nunca solas. una tasa de `5,2%` no dice nada, una de `5,2% (3/58)` sí. con menos de unos 100 jugadores activos en la ventana, lo que sale es orientativo.

## qué muestran los datos

las tres semanas completas, del 21 de septiembre al 11 de octubre, dicen esto:

- el 72,4% de las respuestas son correctas: 19.161 de 26.452. en las fichas, el 73,9% (9.780 de 13.235), y en los perfiles, el 71,0% (9.381 de 13.217).
- la respuesta mediana tarda 1,76 s, y el p90, 6,6 s. las respuestas en menos de un segundo son correctas el 71,5% de las veces (3.006 de 4.203), y las de 3 s o más, el 68,1% (4.998 de 7.334). las caras difíciles hacen pensar más, así que esto no demuestra que pensar perjudique.
- de 2.203 partidas empezadas, se han terminado 1.511, el 68,6%.

![gráfico de barras de las partidas empezadas por día de la semana del 21 de septiembre al 11 de octubre de 2026. lunes 273, martes 300, miércoles 553, jueves 748, viernes 227, sábado 67 y domingo 35.](https://joeltaylor.business/_next/static/media/setmana.es.1-1g4pqxf3gf2.webp)

_el juego se juega entre semana: sábado y domingo suman 102 de las 2.203 partidas, el 4,6%. partidas empezadas por día de la semana, en utc, del 21 de septiembre al 11 de octubre de 2026, según la base de datos de profileorwanted.com._

el 8 de octubre hubo 529 partidas, y el 11, que era domingo, 8. umami tiene 1.029 visitantes únicos, y 608 vienen de bing y 604 usan edge. todo apunta a ordenadores del trabajo o del colegio. el recuento de umami es un mínimo, porque el seguimiento no contó nada del 20 al 22 de septiembre. el detalle de las búsquedas de bing está en [el otro artículo](/blog/profile-or-wanted-juego-linkedin-interpol).

la mayoría no vuelve: de los 632 jugadores que empezaron su primera partida entre el 21 de septiembre y el 10 de octubre, 18 (2,8%) jugaron otra al día siguiente. la línea de base de antes de tocar nada era del 3% al 7%. son pocas personas, así que lo leo como orientativo.

## qué cambiaría

lo primero es que las 387 partidas del 21 al 25 de septiembre se jugaron cuando la respuesta iba al navegador. son el 17,6% de las 2.203. no sé si alguien miró la red, pero la respuesta estaba, y esas partidas entran en los números de este artículo.

lo segundo es hacer un intake de perfiles, para que las caras a prueba no sean todas fichas y una cara desconocida no delate la respuesta.

lo tercero es la retención. un 2,8% al día siguiente quiere decir que la mayoría juega una sola vez, y es la cifra que hay que mover.

el juego está en [profileorwanted.com](https://profileorwanted.com), en catalán, castellano, inglés y francés.
