Zymplo · Canal propio · Reunión de equipo

Zy Chat: la app de Zymplo como canal propio

El mismo cerebro que responde en WhatsApp, ahora también dentro de la app — con la misma conversación, sin pagarle a Meta por cada mensaje. Todo está construido y probado, detrás de un interruptor apagado. Este informe explica qué tenemos, qué hace, qué falta y qué hace falta para encenderlo.

Interruptor APAGADO · cero cambio en prod 3906 tests verdes · 0 rojos PR #2343 → develop Review adversarial: 9/9 hallazgos arreglados Motivo: Meta cobra servicio desde 1-oct-2026
01

Qué construimos

El concepto

No es otra app con menús. Es el mismo chat de siempre: el MEI ya sabe usar WhatsApp, así que la app le da exactamente esa experiencia — más lo que WhatsApp no puede dar (historial visual, avisos instantáneos gratis, todo en un lugar).

💬 WhatsApp

  • El MEI entra por acá (puerta de entrada)
  • Cada mensaje le cuesta a Zymplo
  • Sin historial visual, sin gráficos
MISMO AGENTE
MISMO HILO
167 herramientas
hilo = user + empresa

📱 Zy Chat (app)

  • La conversación continúa acá, con todo el contexto
  • Push gratis en vez de mensaje pago
  • Historial infinito, tarjetas, voz y foto

Empezás una conversación en WhatsApp y la seguís en la app (y al revés) sin perder nada: el hilo es el mismo (user_{id}_tenant_{empresa}), así que la memoria es compartida gratis.

02

Qué tenemos y qué hace cada pieza

7 bloques · D1–D7
D1 · PUSH

Avisos gratis por la app

Tabla de dispositivos + cliente Expo + AppPushClient. El aviso se escribe en el historial y se manda como push. Reemplaza la plantilla de WhatsApp que cuesta plata.

D2 · IDENTIDAD

Pase por usuario (JWT)

/app/token cambia la sesión de Oracle por un pase corto atado a (teléfono, empresa). Reemplaza el token fijo que venía metido en el binario. Convive con el viejo para no romper la app publicada.

D3 · HISTORIAL

Misma conversación en los dos lados

/app/chat/history guarda todo y espeja los turnos de WhatsApp. El MEI ve la charla completa, no importa por dónde escribió.

D4 · INTERFAZ

Botones, listas y tarjetas

Los mismos payloads que el bot genera para WhatsApp, mostrados nativos en la app: botones como chips tocables, listas y tarjetas de confirmación.

D5 · MEDIA

Nota de voz y foto de recibo

/app/chat/media: el audio pasa por la misma transcripción y la foto por la misma visión/OCR que WhatsApp. La prueba de fuego: “terminei o serviço do João, cobra 350” por voz → registra el cobro.

D6 · RUTEO + COSTO

Push primero, WhatsApp de respaldo

channel_router intenta el push gratis y cae a WhatsApp si hace falta. Además un medidor de costo real de Meta por usuario (wa_send_ledger) — la base para recalibrar el precio el 1-sep.

D7 · PRECIO

Dos precios visibles + arreglo de una contradicción

Motor de precio por canal (el mismo plan más barato en la app, financiado por el ahorro de Meta) y la línea del ahorro en el paywall. De paso arreglamos un bug real: el paywall decía R$39 y la tabla cobraba R$19,90 — el MEI veía un precio y pagaba otro.

03

La revisión de seguridad

15 agentes · 9 hallazgos · todos arreglados

Antes del PR corrimos una revisión adversarial del código completo. Salieron 9 problemas reales (los verifiqué uno por uno contra el código antes de tocar nada). Todos arreglados, con tests que los fijan.

🛑

El más grave — y por qué importa

El endpoint que emite el pase de la app aceptaba cualquier número de teléfono que le mandaran. Como los MEIs publican su WhatsApp, alguien logueado podía pedir un pase con el número de otro y leerle la conversación entera o robarle los avisos. Arreglado: ahora el servidor verifica, con su propia llave, que ese teléfono pertenece de verdad a la empresa de quien pide el pase.

Sev.Qué pasabaArreglo
CRÍTICO Pase (JWT) emitido para cualquier teléfono → robo de identidad entre empresas El servidor ata teléfono→empresa con su service-key y exige que coincida. Falla cerrado.
ALTO Token viejo (en binarios publicados) leía historial ajeno en los endpoints nuevos Los endpoints nuevos exigen pase por usuario; el token viejo queda solo para el chat ya publicado.
ALTO Recordatorio dado por “enviado” con solo el push aceptado → se perdía para siempre si el device tenía notifs apagadas Push solo best-effort fuera de ventana; dentro de ventana WhatsApp sigue garantizado y reintenta.
MEDIO El ruteador daba por entregado un aviso sin fila de historial Ante error cae a WhatsApp, sin importar la prioridad.
MEDIO Chat con pase sin empresa caía al empresa del body Falla cerrado (400) — nunca confía en el body.
MEDIO Registro de device sin teléfono en la huella → re-emparejar a otro número no re-registraba El teléfono entra en la huella.
BAJO El device se ataba al empresa del body Se ata a la empresa del pase verificado.
04

Qué tiene que hacer cada uno

Repartición clara
V
Infra · DBA · Deploy
Victor
  1. Aplicar 2 migraciones en la base del bot: 028_app_channel.sql (dispositivos + mensajes) y 029_wa_send_ledger.sql (medidor de costo). encender
  2. Setear los 2 secretos/flags en prod (openclaw): APP_JWT_SECRET (secreto fuerte, sin él /app/token responde 503) y APP_CHANNEL_ENABLED=true. encender
  3. Android en Play (internal track → producción). Hoy iOS 1.0.21 está publicado; Android no está en Play, y eso bloquea la migración en Brasil. bloqueante
  4. Cerrar el token viejo: tras el update forzado del binario, poner APP_LEGACY_TOKEN_OK=false. después
  5. Checkout web de dos precios en el módulo de planes de zymplo-api (channel_pricing). follow-up
L
Validación · Device · Diseño · Backend
Luz
  1. Verificar el push en un teléfono físico (credenciales EAS/Expo). Es lo que confirma que todo el canal funciona de punta a punta. bloqueante
  2. Smoke E2E cross-canal: escribir en WhatsApp y seguir en la app (mismo hilo), mandar voz y foto, y recibir un push con la app cerrada. encender
  3. Diseño fino de los componentes de la app (chips, bottom-sheet, tarjetas) con la marca. ahora
  4. Wire del “push-ack” en el mobile: hoy la app aún no avisa cuando abre un push. Conectarlo desbloquea los recordatorios realmente gratis. follow-up
  5. Decisión Oracle: exponer el teléfono por usuario en /auth/me para cerrar el caso Equipe (varios usuarios por empresa). follow-up
05

Qué falta para terminar del todo

Follow-ups (no bloquean encender el chat)
momentosM1–M4 en los prompts — los avisos que invitan a bajar la app (tras el primer recibo, al pedir historial, en el paywall). Se integran en los flags de onboarding que ya existen, sin duplicar.
push-ackConfirmación de lectura + escalación — cuando el mobile avise que vio el push, activamos el ahorro real de los recordatorios y la escalación push→WhatsApp temporizada.
equipeTeléfono por usuario — para el plan Equipe (varios usuarios en una empresa). Hoy el pase se ata a la empresa; para MEI normal (un dueño = una empresa) está 100% cerrado.
checkoutWeb de dos precios — mostrar ambos precios en el checkout MercadoPago/Pix (la app nunca vende adentro, para no pagar 15–30% a Apple/Google).
viralBonus de instalación + K-factor — el invitado instala y empareja → el que invitó gana un mes al precio-app. Se apila sobre el motor de referidos vivo.
idiomasStrings en inglés — completar el trilingüe en las pantallas nuevas de la app.
06

Cómo encenderlo y empezar a usarlo

Secuencia exacta
Aplicar las migraciones 028 y 029

Crea las tablas de dispositivos, mensajes y el medidor de costo. (Victor)

Configurar APP_JWT_SECRET

Un secreto fuerte de 32+ bytes. Sin él, el endpoint del pase responde 503 a propósito. (Victor)

Poner APP_CHANNEL_ENABLED=true

Recién acá los endpoints /app/* dejan de responder 404. (Victor)

Verificar el push en un teléfono físico

El paso que confirma que el canal funciona de verdad. (Luz)

Smoke E2E cross-canal

WhatsApp ↔ app mismo hilo, voz, foto y push con la app cerrada. (Luz)

Encender el precio-app cuando se lance opcional

APP_PRICE_DISCOUNT_PCT=40–50 (preço de lançamento · decisión de Carlos).

Cerrar el token viejo

Tras el update forzado del binario: APP_LEGACY_TOKEN_OK=false. (Victor)

📅

Checkpoint 1-sep-2026

Meta publica las tarifas por país. Ahí recalibramos el descuento de la app con los datos reales del medidor que dejamos construido (wa_ledger.monthly_cost). El motivo de todo esto: desde el 1-oct Meta cobra los mensajes de servicio que hoy son gratis.