TomaNota
Un asistente que toma los pedidos por WhatsApp sin poder inventar un precio.
Producto propio para comerciosEn producción

- pruebas automáticas
- +900
- conversaciones simuladas contra el modelo real
- 1.203
- herramientas para el modelo, ninguna acepta un precio
- 8
- tablas con permisos por comercio (RLS)
- 46
El problema
La pizzería, la rotisería o la carnicería de barrio vende por WhatsApp. El cliente escribe como habla (“dos grandes y una mitad muzza mitad napo”), pregunta por el envío y espera que alguien le confirme. Ese alguien es el dueño, que al mismo tiempo atiende el mostrador.
En la hora pico los mensajes se acumulan, los pedidos se anotan en papel con errores en las mitades y en los totales, y a fin de mes nadie sabe cuánto entró por ese canal. Las apps de delivery cobran comisión por clientes que ya son del comercio, y un bot de respuestas armadas no entiende cómo escribe la gente.
Qué construí
TomaNota, un producto propio con tres puertas de entrada que terminan en un mismo tablero:
- Un asistente de WhatsApp que conversa en lenguaje natural, arma el pedido con la carta real del comercio, muestra un resumen y espera que la persona toque Confirmar. Si el cliente pide hablar con alguien, o el dueño escribe desde su teléfono, se hace a un lado.
- Una carta online con QR, para el cliente que prefiere elegir sin escribir.
- Pedidos de mostrador, para lo que entra en persona o por teléfono.
- La pantalla del comercio: el tablero en tiempo real con cada estado, el catálogo, las zonas de envío, los textos del asistente y un informe del mes por canal.
Por dentro son tres piezas. Un motor de conversación en TypeScript sin red ni disco, que corre igual en producción y en los tests. Un servidor en Fastify que lo conecta con WhatsApp, con Redis y con el modelo, a través de Vercel AI SDK sobre una API compatible con OpenAI. Y PostgreSQL en Supabase, con seguridad por fila en las 46 tablas: cada comercio ve solo lo suyo, y el servidor entra con un rol que no puede saltear esos permisos.
Antes de que intervenga la IA, cada mensaje pasa por controles fijos: un turno por chat a la vez (dos globos seguidos suman las dos cosas al carrito), descarte de reintentos del webhook, silencio cuando escribe el dueño y un corte a las 60 respuestas por hora que frena a dos bots contestándose en loop.
Lo más difícil
Que el modelo no pueda inventar un monto
Si el asistente dice un precio distinto al de la carta, el comercio pierde plata o discute con un cliente que tiene la captura. Lo resolví por diseño: el modelo no tiene ningún camino para escribir un precio en el pedido.
Trabaja con 8 herramientas cerradas (agregar, mitad y mitad, extra, cambiar cantidad, quitar, envío, mostrar el carrito y avisar que está listo) y solo emite ids de catálogo y cantidades. Un producto agotado ni figura entre los ids válidos. El servidor ejecuta cada herramienta, calcula y escribe el resumen: el texto que la persona confirma nunca lo redacta el modelo.
Al confirmar, una única función de la base vuelve a cotizar todo con los precios de ese momento, con una clave de idempotencia para que un reintento no duplique el pedido. Bot, carta y mostrador pasan por esa misma función.
La media pizza lo puso a prueba: en una carta real, la media no es la mitad de la entera. Encontré cuatro lugares que dividían por dos cuando faltaba el dato, incluida la regla que medía al asistente. Hoy la media tiene precio propio y, si no está cargado, ese producto se vende solo entero.
La confirmación es de la persona
La herramienta que avisa “listo” no confirma nada: le pide al sistema que muestre el resumen con Confirmar, Modificar y Cancelar. Ningún pedido existe hasta que la persona toca Confirmar. Y como el modelo puede escribir “pedido confirmado” por su cuenta, una regla cruza cada frase de cierre contra los pedidos realmente guardados en ese turno.
Cómo se prueba
Cinco capas, cada una para un tipo de error distinto:
- Más de 900 pruebas automáticas: 847 en la suite rápida y 88 de integración contra PostgreSQL real, con el rol del servidor. Una de ellas recorre todas las funciones de la base y falla si alguna queda ejecutable sin permiso explícito.
- 13 conversaciones completas guardadas como especificación, en cada cambio.
- 21 suites SQL que prueban la base desde adentro: el aislamiento entre comercios y que las tres entradas cobren lo mismo.
- Un banco de pruebas contra el modelo real, con 77 casos y 11 reglas que revisan cada respuesta: no inventa precios, no promete medios de pago que no hay, no anuncia un pedido sin confirmar. Suma 1.203 conversaciones simuladas, difíciles a propósito: inyección de prompt, descuentos que no existen, una mitad pizza mitad empanada.
- GitHub Actions en cada push: escaneo de secretos sobre toda la historia, chequeo de tipos y la suite rápida.
Cómo funciona
- 01WhatsAppfirma verificada
- 02Orden y duplicadoscorte ante loops
- 03Modelo de lenguaje8 herramientas
- 04Servidor calculaprecio del catálogo
- 05Resumenlo arma el sistema
- 06Confirmarlo toca la persona
- 07Base de datosvuelve a cotizar
- 07Carta con QRel cliente elige
- 07Mostradorpersona o teléfono
- 08Tableroen tiempo real
Resultados
Tres entradas, un solo precio
WhatsApp, la carta con QR y el mostrador cotizan con la misma función de la base y cobran siempre lo mismo.
El dueño toma el control
Si escribe desde su teléfono en un chat, el asistente se calla en esa conversación durante 30 minutos.
Nunca queda mudo
Con un tope de 20 segundos por llamada y 45 por turno, el cliente pasa a una persona y el dueño recibe el motivo.
17 de 17 direcciones
La zona de envío se deduce de la dirección con tramos de calle y contornos definidos por las calles que la limitan.
Como vende cada comercio
Tamaños, extras con mínimo y máximo, mitad y mitad, media pizza con precio propio y venta por peso.
Cinco idiomas
Detecta el idioma del cliente y lo sostiene durante todo el pedido.
Galería
Stack
- Aplicación
- React 18ViteTypeScriptTailwind CSSTanStack QueryPWALeaflet
- Servidor e IA
- Node.js 22FastifyVercel AI SDKAPI compatible con OpenAITool callingWhatsApp Cloud APIRedis
- Datos
- SupabasePostgreSQLRow Level SecurityRealtimeFunciones plpgsql
- Calidad y despliegue
- VitestSuites SQLGitHub ActionsgitleaksDocker ComposeVercel





