~/henry-cabello
CVEnglish
← volver a ./work
En desarrollo

Mate

Un matchmaker con IA para romance, amistad y grupos pequeños. Sin swipe: un job programado lee lo que le contaste sobre ti, puntúa candidatos en las dimensiones que la investigación dice que sí predicen compatibilidad, y te entrega un match con una razón escrita. Un incidente en producción, y una validación de mercado que mató el encuadre que le daba nombre al producto.

Rol
Solo: producto, investigación, backend, front-end, sistema de diseño
Ventana de construcción
2025-10-28 → 2026-08-15, 291 commits
Corre en
Vercel, Supabase Postgres, Inngest, Ably
Modelos
Gemini Flash para chat, Llama 3.3 70B en Groq para análisis

Repositorio privado. Con gusto lo recorro en una llamada.

La landing de MateLa pantalla de matches, cada uno con su puntaje de compatibilidadUna conversación de match en la app
La puerta de entrada, los matches que el motor produjo de noche, y una conversación de match. Los builds nativos salen del mismo codebase con Capacitor.

$ cat README.md

El swipe optimiza por fotos y volumen. La gente se está yendo.

Tinder perdió suscriptores pagos once trimestres seguidos, los pagos de Bumble cayeron interanual, y las encuestas de hartazgo siguen dando lo mismo. El éxodo va hacia productos que producen un resultado, no hacia un mazo de cartas mejor.

Mi tesis en octubre de 2025: si un modelo puede aprender quién eres a partir de una conversación normal, puede evaluar compatibilidad en valores, metas de vida y estilo de comunicación, y hacerlo de forma continua en segundo plano, sin que califiques desconocidos por foto.

Así que el onboarding es una conversación con un esquema detrás. Diez temas, cada uno con preguntas guía, los campos exactos de base de datos que alimenta, y pistas de extracción, todo en un JSON que un admin edita sin desplegar. Los temas se ramifican según lo que dices: dile que quieres algo casual y deja de preguntar por plazos de matrimonio. Cuando termina la conversación, una segunda pasada del modelo lee la transcripción y escribe unos treinta campos estructurados, convirtiendo "no sirvo antes de las 10am" en morningNight: 3.

También puedes saltártelo. Te doy un prompt, lo pegas en el asistente con el que ya hablas todos los días, resume lo que ha aprendido de ti en meses de conversación real, y pegas la respuesta de vuelta. El perfil se arma con evidencia que generaste sin actuar para una app de citas, que es exactamente la evidencia que una app de citas nunca puede recoger de ti por su cuenta.

$ ./bin/report --totals

661Matches del job, casi todos entre cuentas sembradas
291Commits
8Jobs programados
25Modelos
~200Tokens por par que lee el matcher

Contado el 15 de agosto de 2026: usuarios y matches desde la base de datos desplegada, el resto desde el repositorio. Lee la cifra de usuarios con cuidado: son cuentas registradas, y el esquema no lleva ninguna bandera que separe a los usuarios sintéticos que crea el seeder de los demás, que es justo la lección que enseña la sección de egress más abajo. Hay un entorno desplegado con base de datos y crons corriendo, y no hay lanzamiento público, así que nadie llegó a esta app desde un anuncio. Los matches son los que produjo el job por hora solo; ninguno salió de un swipe.

$ ls -la ./engineering

La compatibilidad son tres algoritmos, no uno

Lo que hace bueno un romance no es lo que hace buena una amistad, y la literatura lo dice sin rodeos. Los pesos están separados por tipo de conexión, y cada uno lleva su fuente en el comentario de arriba: un meta-análisis de PNAS con 11.000 parejas, el trabajo de Campbell sobre química en la amistad, la investigación de 2025 sobre formación de comunidades. Un peso de comunidades está multiplicado por cero, con un comentario que explica que la evidencia de 2025 dejó de sostenerlo. Dejé la función y puse el peso en cero en vez de borrar el razonamiento.

El problema de costo, y el arreglo del que estoy más orgulloso

Comparar dos personas significa poner a las dos frente a un modelo. Lo obvio es mandar todo lo que le han dicho al asistente, unos 10.000 tokens por par. Los pares crecen de forma cuadrática, así que ese diseño es insolvente cerca de los cien usuarios. Así que las conversaciones nunca llegan al modelo de matching. Un job nocturno lee los mensajes recientes de cada usuario y escribe un resumen estructurado pequeño, guardado en tres cubetas que envejecen. El matcher solo ve los resúmenes. La misma decisión sobre la cincuentava parte de la entrada, de 10.000 tokens a unos 200, porque el resumen corre una vez por usuario por día en vez de una vez por par por análisis, y los usuarios crecen de forma lineal donde los pares no.

// what the matching model reads, per pair
naive   ████████████████████████████████████████  ~10,000 tokens   every message both people ever sent, never built
after   █                                         ~200 tokens      last week, rewritten nightly at 03:00
        ~150   last month, folded monthly at 04:00      computed, not yet read
        ~100   long-term, folded quarterly at 05:00     computed, not yet read

Dos de las tres cubetas hoy solo se escriben. Los plegados mensual y trimestral corren en horario y guardan su salida, pero el matcher por hora sigue inyectando solo la cubeta semanal. La compresión es real y el ahorro también; la mitad de memoria larga está construida y todavía no conectada a lo que la usaría.

Nunca rehén de un proveedor

La capa de IA está abstraída por propósito, no por proveedor: un modelo barato y rápido para conversación, uno fuerte para análisis, cada uno elegido por variable de entorno. Hoy son Gemini Flash y Llama 3.3 70B en Groq, elegidos porque el tier gratis de Groq permite 30 requests por minuto contra los dos de Gemini. El manejador de reintentos lee la pista del propio proveedor desde el string del error, y existe porque me topé con 429 y 503 repetidamente en producción.

Verificación de identidad que no cuesta nada

Todo proveedor de verificación cobra por chequeo, así que la verificación corre entera en el navegador. face-api.js carga sus modelos como WebAssembly, saca un descriptor de 128 dimensiones de la cámara en vivo, hace lo mismo con la foto de perfil, y los compara por distancia euclidiana. La prueba de vida es un gesto: sonríes. El video nunca sale del dispositivo, y no se guarda más que un booleano y una marca de tiempo.

El motor de comparación son 522 líneas de matemática determinista

El modelo de lenguaje maneja el matiz. No maneja las partes donde la investigación ya da una respuesta: esas son código, y cada una lleva el estudio del que salió en el comentario de arriba. Valores e intereses por similitud de Jaccard, con impulso cuando ambos marcan lo mismo como importante. Metas de vida en un modelo de penalización, donde una discrepancia sobre hijos cuesta 60 puntos si alguno la declara dealbreaker. Estabilidad emocional de forma asimétrica: ambos por encima de 7 puntúa entre 97 y 100, y cualquiera por debajo de 3 se topa en 30. Comunicación por un grafo de complementariedad de lenguajes del amor. Disponibilidad como traslape en seis franjas horarias, con un piso para que cero traslape no sea fatal.

Next.jsTypeScriptReactPrismaPostgresSupabaseInngestAblyCapacitorGemini FlashGroqface-api.jsPlaywrightJestVercel
La vista de match de Mate con la explicación de compatibilidad y el asistente
La vista de match. A la izquierda, la explicación del propio modelo sobre el 94% con la conversación debajo. A la derecha, un asistente que sabe por qué hicieron match. Los dos paneles están vivos en la app.

$ crontab -l

Casi todo este producto corre mientras nadie lo mira.

Nada de lo de abajo tiene pantalla. Ocho jobs programados y una herramienta de reparación bajo demanda leen conversaciones, las comprimen, buscan candidatos, puntúan pares, escriben los artefactos que un match necesita antes de que alguno lo abra, y limpian detrás de ellos. Uno de ellos, conversation-analysis, sirve al subsistema agente-a-agente retirado y hoy corre contra una cola vacía. Debió borrarse cuando cambió la arquitectura.

0 * * * *agent-matchmaking buscar candidatos, puntuar, crear matches
0 3 * * *process-alter-conversations comprimir los últimos 7 días por usuario
0 4 1 * *aggregate-monthly-insights plegar semanal en patrones mensuales
0 5 1 1,4,7,10 *aggregate-quarterly-insights plegar mensual en personalidad estable
0 9 * * *verification-expiry-check vencer, avisar, enfriamiento de 7 días
0 */6 * * *conversation-analysis cola heredada, hoy vacía
0 2 * * *daily-test-seeder detrás de ENABLE_TEST_SEEDER
0 1 * * 0cleanup-test-data borrar fixtures de más de 7 días
on-demandresume-conversation herramienta de reparación, concurrencia 1
El bucle por hora tiene gobernadores de costo, no solo lógica

Para cada usuario activo el job saca 20 candidatos con un filtro SQL (rango de edad, género, tipo de conexión, y exclusión de matches existentes) y después aplica las reglas que no se pueden expresar como preferencia. El interés mutuo se exige en ambas direcciones, así que un match nunca puede ser unidireccional. La verificación funciona como filtro de privacidad: si pides solo matches verificados ves solo gente verificada, y si tú no estás verificado solo te muestran a quienes aceptaron verte. Después, un techo duro de tres llamadas al modelo por usuario por hora, con un segundo de espacio entre ellas. Ese número es un presupuesto.

$ cat docs/EGRESS_ANALYSIS_2025-11-14.md

La base de datos llegó a su límite de egress, y tres cuartas partes de los usuarios que lo movían eran datos de prueba.

El job de matchmaking por hora cargaba cada agente activo con un include relacional completo, y después consultaba veinte candidatos por cada uno, otra vez con includes completos. Ochocientas cargas de perfil por hora. Diecinueve mil al día. Unos 94 MB de egress diarios solo de este job, unos 2,8 GB al mes contra un plan que permite 5,5 GB. El job no era la cuenta entera, era la mitad que yo podía borrar.

Escribí scripts de diagnóstico en vez de adivinar, y los números dijeron algo que no esperaba: 44 de 59 usuarios eran de prueba. Eso es un problema de arquitectura, y se presentó como una alerta de facturación.

La remediación tenía cuatro partes, ordenadas por impacto: borrar los usuarios de prueba, reemplazar cada include por un select explícito que nombre solo los campos que el análisis lee, recortar el lote de candidatos, y bajar la frecuencia del cron.

$ npx tsx scripts/check-db-stats.ts

total users ..........  59
test users ...........  44   ← 75% of the database
active agents ........  40   all matched hourly
matches ..............  35
est. daily egress ....  94 MB  → ~2.8 GB/month from this job alone

Hoy solo está en el código la mitad de la forma de las consultas. El job sigue corriendo cada hora y sigue tomando veinte candidatos. Arreglé la parte cara, dejé la barata, y el documento de análisis sigue en docs/ describiendo trabajo que no terminé. La segunda lección fue sobre límites de tasa: la IA de tier gratis no está libre de restricciones, solo de costo. Un techo de dos requests por minuto le dio forma a toda la arquitectura de fondo: el tope de tres análisis por hora, la espera deliberada entre llamadas, el backoff exponencial. La arquitectura salió de una cuota.

$ cat docs/MARKET_VALIDATION_2026-06.md

Hice la investigación, y mató mi encuadre.

Para junio de 2026 el producto funcionaba, así que dejé de construir y corrí una validación estructurada: tres pasadas paralelas de investigación profunda sobre panorama competitivo, economía de precios y salida al mercado de bajo presupuesto, con verificación adversarial de todo lo que cargaba peso.

Confirmó la mecánica y mató el encuadre. El onboarding conversacional hacia un matchmaker con IA y sin swipe es hoy la apuesta de consenso de la industria: Bumble anunció que quitaría el swipe del todo, el fundador de Hinge se fue a construir una app de citas con IA primero, y cuatro startups financiadas convergieron en la misma forma. Pero la investigación encontró que "agentes de IA representan a los usuarios" es el encuadre con más rechazo de la categoría, y las dos cifras de abajo salen de esa pasada, no de encuestas primarias que yo haya leído. Los productos de agente-como-proxy que pude encontrar habían cerrado o pivotado. El 58% de quienes usan apps de citas llama catfishing a los mensajes escritos por IA, y el 68% de los estadounidenses no deja que una IA actúe sin revisión en su nombre.

La parte incómoda: mi arquitectura ya estaba del lado correcto de esa línea y mi copy no. No hay conversación agente-a-agente en ninguna parte del producto. El modelo de matching lee dos perfiles y dos resúmenes y los puntúa. La IA nunca habla como el usuario, con nadie. Había construido un matchmaker y comercializado un proxy.

SupuestoMi modeloReferencia
Descarga → pago5.0%2.0%
Renovación anualsupuesta alta25%
ARR con 10k usuarios$162,000~$4,000

Dos hallazgos más. Mis proyecciones de ingresos estaban unas 2,5 veces por encima de las medianas de la industria: modelé 5% de conversión premium contra un benchmark de 2,0% en la categoría con peor retención, y una proyección de $162K de ARR estaba más cerca de $4K. Y tres tipos de conexión en una sola app va contra la mejor evidencia disponible, porque Bumble corrió ese experimento ocho años y lo deshizo. El cambio de nombre a Mate salió de acá. El producto ahora dice que la IA filtra el ruido y te manda a conexiones de alta afinidad.

La landing de Mate en inglésLa landing de Mate en español
Landing en inglés y español. Las dos mantenidas a la par en vez de traducidas al vuelo.

$ ls ./built

Qué está construido, y qué no

  • auth por correo y Google, con puerta de 18+
  • onboarding conversacional e importación de historial de IA
  • tres tipos de conexión: romance, amistad, comunidades
  • matchmaking programado con explicaciones escritas
  • chat en tiempo real, indicadores de escritura, confirmaciones de lectura
  • asistente de IA por match y rompehielos
  • verificación de identidad en el navegador con cola de apelaciones
  • bloquear, reportar, cola de moderación
  • exportación y borrado de cuenta GDPR
  • panel de admin con impersonación y siembra
  • notificaciones push, localización EN/ES, iOS y Android nativos

# No construido

  • Pagos. El modelo de precios está investigado, no implementado
  • Correo transaccional. Recuperar contraseña escribe el token en la consola
  • Matching por distancia geográfica. Los campos existen, sin poblar
  • El flujo de aprobación de comunidades. Un TODO está donde debería crearse el registro
  • La Etapa 1 del pipeline de matching, conectada
  • Un despliegue de producción sirviendo tráfico hoy

$ cat method.md

De los primeros 282 commits que atribuí, 155 los escribió un agente de código y 127 fueron míos.

La pregunta interesante es qué hiciste con las partes que no puede hacer.

Lo que el agente de código hizo bien fue amplitud mecánica. En enero de 2026 audité toda la superficie de funciones contra el codebase, escribí los huecos en un archivo de tareas priorizado, y corrí un loop autónomo contra él. En dos iteraciones cerró bloquear y reportar, recuperar contraseña, borrado y exportación GDPR, indicadores de escritura, confirmaciones de lectura, deshacer match, creación de comunidades, una cola de moderación, apelaciones de verificación y vista previa de perfil, cada una con tests y los dos archivos de traducción actualizados.

Lo que no pudo hacer es donde se fue mi tiempo. Decidir cuáles debían ser los pesos: ningún agente de código lee un meta-análisis de 11.000 parejas y concluye que la estabilidad emocional merece 5% en romance y cero en comunidades. Encontrar la causa raíz del incidente de egress, que exigió el instinto de medir antes de optimizar y de sospechar de los datos de prueba. Correr la validación que invalidó mi propia premisa, y después actuar en consecuencia. E ir a buscar lo que faltaba, porque las ausencias nunca aparecen en un diff.

Un agente de código escribe con gusto un módulo correcto y nunca lo conecta. Este codebase tiene un ejemplo. lib/matching/quick-filter.ts es un pre-filtro completo, testeado y de costo cero, documentado como Etapa 1 de un motor de dos etapas, y el job de producción no lo importa. Un test unitario, una exportación de barril, una acción de admin. El pipeline de dos etapas documentado es de etapa y media en producción. Lo encontré escribiendo esta página.

$ cat POSTMORTEM.md

La mecánica está validada. El alcance no.

Lo que funciona: el matching produce matches reales y explicados en horario, a un costo que sobrevive un tier gratis, en dos idiomas, en web y nativo. Lo que la investigación dice que no va a funcionar es la forma que le di, tres tipos de conexión, sin ciudad, sin compuerta de lanzamiento, y un nombre que describía el único encuadre que la categoría rechaza.

La evidencia sobre arranque en frío no es ambigua: una ciudad, una comunidad, unas 150 personas, abierto solo cuando se alcanza el umbral. Ese es el siguiente build, y otra vez es sobre todo borrar.

# Qué haría distinto

  1. Validar el encuadre antes de escribir 68.000 líneas. Tres pasadas de investigación profunda me costaron un día en junio. Habrían costado ese mismo día en octubre y habrían cambiado el nombre del producto, su copy, y probablemente su alcance.
  2. Nunca dejar que los datos de prueba compartan ruta de código con usuarios reales. Que 44 de 59 usuarios fueran de prueba lo aprendí por una alerta de facturación.
  3. Salir angosto. Una ciudad, una comunidad, una compuerta de lanzamiento. Construí para tres tipos de conexión y dos idiomas y cero ciudades.
  4. Conectar la raíz de composición primero. Construir el pipeline de punta a punta con etapas de relleno y después llenarlas, y un módulo correcto pero huérfano se vuelve imposible por construcción.

← volver a ./work