Claude Opus 4.6
claude-opus-4-6
Score por categoría
Evaluaciones
Brain.
2 pruebas
B1.2features82.1
Respuesta del modelo
Diagnóstico y Plan Anti-Churn
1. EL DIAGNÓSTICO REAL
El problema no es el churn. El churn es el síntoma terminal de un problema más profundo:
Tu producto compite contra Excel y pierde, porque estás vendiendo "gestión de inventario" cuando tu cliente necesita "que mi negocio no pierda dinero por descontrol de stock".
Voy a desarmar esto porque las exit interviews, leídas en conjunto, cuentan una historia muy clara:
La cadena causal real
Carga manual producto por producto
↓
El primer mes el cliente carga su inventario (con entusiasmo de novedad)
↓
En el día a día, registrar cada movimiento toma MÁS tiempo que su Excel
↓
El dueño delega al empleado → el empleado no lo adopta (fricción alta)
↓
Los datos quedan incompletos → los reportes y alertas son inútiles
↓
El producto no genera valor perceptible → "no vale $45"
↓
Vuelve a Excel (que es "suficientemente bueno" para su escala)
El problema raíz es uno solo: el costo operativo diario de usar tu producto es mayor que el beneficio percibido. No es un problema de features faltantes. Es un problema de que el loop central del producto — registrar movimientos de inventario — es más doloroso que la alternativa.
Los 5 que volvieron a Excel, los 3 cuyo empleado no lo usó, y los 2 que dijeron "no vale el precio" son el mismo problema visto desde tres ángulos. Solo los 2 de integraciones con POS son un problema diferente (y secundario).
Un dato que debería alarmarte más que el churn
Si 5 de 12 usaron el producto solo el primer mes, tu time-to-value es negativo: el cliente invierte tiempo en el onboarding y nunca recupera esa inversión con valor real. Eso significa que probablemente tengas otros 40-50 clientes activos ahora mismo que ya dejaron de usar el producto pero aún no cancelaron. Tu churn real es peor de lo que ves.
2. LAS 8 IDEAS, PRIORIZADAS POR IMPACTO EN CHURN
IDEA 1: Carga masiva por foto/escáner (captura rápida de inventario)
Prioridad: #1 — CRÍTICA
| Dimensión | Detalle | |---|---| | Qué es | Permitir escanear códigos de barras con la cámara del celular para dar entrada/salida de productos. Para productos sin código, permitir carga por foto de lista/factura del proveedor (OCR básico). Carga por CSV/Excel como mínimo viable inmediato. | | Señal que ataca | "Volví a mi planilla — me sale más rápido" (5/12) + "Mi empleado no supo usarlo" (3/12) | | Hipótesis | Si reducimos el tiempo de registro diario de 15 min a 2 min, el producto deja de perder contra Excel en el uso cotidiano. El escaneo elimina la barrera de capacitación del empleado: apuntar la cámara no requiere training. | | Impacto esperado | Alto. Ataca la causa raíz directa del 67% de las salidas. | | Esfuerzo | M — El import CSV es S (1-2 semanas). El escáner de código de barras con cámara web tiene librerías maduras (QuaggaJS, ZXing). OCR de facturas es M-L pero puede empezarse con un MVP de "foto + corrección manual". |
IDEA 2: Onboarding asistido con migración de Excel del cliente
Prioridad: #2 — CRÍTICA
| Dimensión | Detalle | |---|---| | Qué es | En lugar de que el cliente cargue productos uno por uno, el onboarding empieza con "subí tu Excel actual". El sistema mapea columnas automáticamente, importa todo, y el cliente arranca con datos reales desde el día 1. Incluir una sesión de 20 min por videollamada en la primera semana. | | Señal que ataca | "Volví a mi planilla" (5/12) + "No vale el precio" (2/12) | | Hipótesis | El primer mes es donde se pierde al cliente. Si arranca con su inventario ya cargado y ve un reporte real de SU negocio en la primera semana, el time-to-value pasa de "nunca" a "día 3". Esto cambia fundamentalmente la percepción de valor. | | Impacto esperado | Alto. No requiere desarrollo complejo y ataca el momento más crítico del ciclo de vida. | | Esfuerzo | S — El import CSV es desarrollo menor. La videollamada es esfuerzo operativo (tú o alguien del equipo), no de desarrollo. Con 180 clientes y ~15-20 nuevos/mes, es manejable. |
IDEA 3: Modo empleado simplificado ("Modo Caja")
Prioridad: #3 — ALTA
| Dimensión | Detalle | |---|---| | Qué es | Una vista ultra-simplificada para el empleado: solo buscar producto (por nombre o escáner), marcar entrada o salida, poner cantidad. Sin reportes, sin configuración, sin nada que distraiga o confunda. Acceso por PIN de 4 dígitos, sin crear cuenta. | | Señal que ataca | "Mi empleado no supo cómo usarlo" (3/12) | | Hipótesis | El empleado de un comercio minorista en México típicamente no tiene experiencia con software empresarial. Si la interfaz del empleado tiene 3 botones en lugar de un dashboard completo, la adopción sube dramáticamente. Y si el empleado lo usa, los datos son confiables, y el dueño ve valor en los reportes. | | Impacto esperado | Alto. Resuelve el problema de adopción organizacional que es prerequisito para que el producto genere valor. | | Esfuerzo | S-M — Es principalmente trabajo de diseño (nueva vista) con un sistema simple de roles/permisos. |
IDEA 4: Dashboard de "dinero en riesgo" en tiempo real
Prioridad: #4 — ALTA
| Dimensión | Detalle | |---|---| | Qué es | Reemplazar el reporte mensual estático por un dashboard en vivo que muestre: "Tienes $X en productos que no se mueven hace 60 días", "Vas a quedarte sin [producto Y] en Z días y es tu 3er producto más vendido", "Perdiste $X en ventas estimadas por falta de stock este mes". Traducir inventario a pesos. | | Señal que ataca | "No vale lo que obtengo" (2/12) + "Volví a Excel" (5/12) | | Hipótesis | Excel no puede decirte cuánto dinero estás perdiendo. Este es el valor diferencial que justifica los $45/mes. Si el dueño abre la app y ve "estás perdiendo $3,200/mes en ventas por desabasto", el producto se paga solo y el cliente lo sabe. Pasa de "herramienta de registro" a "herramienta que me hace ganar dinero". | | Impacto esperado | Medio-Alto. Depende de que haya datos suficientes (requiere que Ideas 1-3 funcionen para tener datos confiables). | | Esfuerzo | M — Requiere lógica de cálculo de rotación, proyecciones simples, y un dashboard bien diseñado. El diseñador part-time es clave aquí. |
IDEA 5: Alertas por WhatsApp (no por email)
Prioridad: #5 — MEDIA-ALTA
| Dimensión | Detalle | |---|---| | Qué es | Migrar las alertas de stock bajo de email a WhatsApp. El dueño de un comercio minorista en México vive en WhatsApp, no en su bandeja de email. Incluir la posibilidad de responder "OK" para marcar como atendido o "PEDIR" para generar una nota de pedido al proveedor. | | Señal que ataca | "Volví a Excel" (5/12) + "No vale el precio" (2/12) | | Hipótesis | Las alertas por email probablemente no se leen. Una alerta por WhatsApp que dice "Te quedan 5 unidades de [producto estrella], ¿le pedimos al proveedor?" es valor tangible que el cliente ve sin abrir la app. Mantiene el producto presente en la vida diaria del cliente. | | Impacto esperado | Medio. No resuelve la causa raíz pero aumenta el engagement y la percepción de valor significativamente. | | Esfuerzo | S-M — La API de WhatsApp Business tiene costo ($0.03-0.05 por mensaje) pero es técnicamente sencilla. Hay que evaluar el costo por cliente vs. el impacto en retención. A $45/mes de revenue, el ROI es claro. |
IDEA 6: Integración con Clip y Conekta
Prioridad: #6 — MEDIA
| Dimensión | Detalle | |---|---| | Qué es | Conectar con los terminales de punto de venta más usados en México (Clip, Conekta, posiblemente Mercado Pago Point) para que las ventas descuenten inventario automáticamente. | | Señal que ataca | "No se conecta con mi punto de venta" (2/12) | | Hipótesis | Para los clientes que ya tienen POS digital, esta integración elimina la necesidad de registrar salidas manualmente (ataca la causa raíz por otro camino). Además, es un diferenciador competitivo fuerte. | | Impacto esperado | Medio. Solo 2/12 lo mencionaron explícitamente, pero resuelve el registro automático de salidas para un segmento específico. Puede ser un argumento de venta fuerte para adquisición. | | Esfuerzo | L — Las APIs de Clip y Conekta existen pero la integración robusta (mapeo de productos, manejo de errores, reconciliación) es trabajo significativo. Mínimo 4-6 semanas para una integración bien hecha. |
IDEA 7: Plan "Starter" gratuito o a $19/mes con upgrade claro
Prioridad: #7 — MEDIA
| Dimensión | Detalle | |---|---| | Qué es | Crear un tier de entrada a $19/mes (o free con límite de 50 productos) que incluya lo básico: inventario + alertas WhatsApp. El plan de $45 incluye reportes avanzados, dashboard de dinero en riesgo, integraciones POS, múltiples usuarios. | | Señal que ataca | "No vale lo que obtengo" (2/12) | | Hipótesis | Algunos comercios son muy pequeños para justificar $45/mes. En lugar de perderlos completamente, retenerlos a $19 genera revenue ($19 > $0) y les da tiempo para crecer al tier completo. Además, reduce la fricción de entrada para nuevos clientes. | | Impacto esperado | Medio-Bajo en churn directo, pero protege revenue a largo plazo. | | Esfuerzo | S — Principalmente decisión de negocio + lógica de billing/permisos. |
IDEA 8: "Inventario colaborativo" — el proveedor actualiza el stock
Prioridad: #8 — EXPLORATORIA
| Dimensión | Detalle | |---|---| | Qué es | Generar un link único por proveedor. Cuando el proveedor entrega mercancía, escanea un QR en la tienda y confirma qué entregó. El inventario se actualiza automáticamente sin que el dueño ni el empleado hagan nada. | | Señal que ataca | "Volví a Excel — me sale más rápido" (5/12) | | Hipótesis | Si las entradas de inventario se registran sin esfuerzo del cliente, se elimina la mitad de la carga operativa. Es un enfoque radical al problema de fricción. | | Impacto esperado | Potencialmente alto pero incierto. Depende de la disposición del proveedor a participar. | | Esfuerzo | M — Técnicamente no es complejo (link público + formulario simple), pero la adopción por parte de proveedores es una incógnita de go-to-market, no de producto. |
3. RESUMEN DE PRIORIZACIÓN
IMPACTO EN CHURN
Alto ─────────────────── Bajo
│ │
Esfuerzo Bajo ── │ ★ #2 Onboarding+Excel │
(S) │ ★ #3 Modo Empleado │ #7 Plan Starter
│ │
Esfuerzo Medio ── │ ★ #1 Carga por escáner │
(M) │ #4 Dashboard $$ │ #8 Inventario colab.
│ #5 WhatsApp │
│ │
Esfuerzo Alto ── │ │
(L) │ #6 Integraciones POS │
│ │
Secuencia recomendada para las próximas 12 semanas:
| Semanas | Acción | |---|---| | 1-2 | #2 (Onboarding con migración Excel) — impacto inmediato en nuevos clientes | | 2-4 | #1 MVP (Import CSV + escáner de código de barras) — reduce fricción diaria | | 3-5 | #3 (Modo Empleado) — desbloquea adopción organizacional | | 5-7 | #5 (Alertas WhatsApp) — aumenta engagement sin requerir que abran la app | | 6-10 | #4 (Dashboard de dinero en riesgo) — cambia la propuesta de valor | | 8-12 | #6 (Integración Clip) — solo una integración, la más demandada |
4. LA IDEA QUE NO HARÍA (AUNQUE SUENA ATRACTIVA)
❌ App nativa (iOS/Android)
Por qué suena atractiva:
- "El dueño y el empleado viven en el celular"
- "Una app nativa se siente más profesional"
- "Podríamos usar la cámara nativa para escaneo"
- "Push notifications en lugar de email"
- Los competidores probablemente la tienen
**Por qué NO la haría
Criterios evaluados
Elige como 'no haría' una app nativa, que es tentadora pero no está anclada a las razones de churn provistas (nadie la mencionó) y la justificación queda incompleta/truncada. Hubiera sido más sólido descartar algo directamente sugerido por interviews (p. ej., priorizar integraciones POS 2/12 o bajar precio 2/12) con argumento 2/12 vs 5/12+3/12 y costo de oportunidad.
Elige descartar la app nativa, que es tentadora pero no directamente conectada con los exit interviews — ninguno de los 12 clientes mencionó la falta de app nativa como razón de salida. La respuesta además está truncada y no completa la justificación. Una elección más potente habría sido descartar las integraciones POS (solo 2/12 la pidieron, alto esfuerzo L, distrae del problema real de fricción) o bajar el precio. La app nativa es una elección 'segura' pero menos incisiva como anti-recomendación basada en los datos específicos del caso.
Identifica bien la causa raíz como fricción/costo operativo y pérdida contra Excel (costo de cambio + time-to-value negativo), conectando coherentemente 5/12 + 3/12 + 2/12. También deja claro que el precio es más bien un reflejo de valor no percibido, aunque podría haber explicitado aún más por qué “bajar precio” sería un error como estrategia principal.
Diagnóstico excelente. Identifica con precisión que el problema raíz es que el costo operativo diario supera el beneficio percibido, unificando las señales de 5/12 (Excel), 3/12 (empleado) y 2/12 (precio) como manifestaciones del mismo problema de fricción. La cadena causal es brillante y el insight de que 'tu churn real es peor de lo que ves' (clientes zombi) demuestra profundidad analítica. Argumenta explícitamente que el precio no es el problema real sino la consecuencia de no percibir valor. Solo pierde puntos por no ser aún más explícito en decir 'bajar el precio sería un error'.
La priorización es coherente con los datos: las ideas #1, #2 y #3 atacan directamente los problemas de 5/12 y 3/12. Las integraciones POS (2/12) quedan correctamente en posición #6. La secuencia de 12 semanas es lógica y bien argumentada. Cada idea indica explícitamente qué señal de exit interview ataca. Pequeña inconsistencia: la idea #1 (escáner) está como prioridad máxima pero la #2 (onboarding Excel) se recomienda hacer primero en la secuencia, lo cual tiene sentido práctico pero genera leve confusión con el ranking declarado.
Prioriza razonablemente soluciones que atacan 5/12 (Excel/velocidad) y 3/12 (empleado) antes que POS (2/12), y lo justifica con frecuencia (“67%”). Hay algunas inconsistencias: #1 mezcla OCR/foto (más grande) con CSV (mínimo), y #4/#5 quedan algo arriba sin cuantificar impacto relativo ni dependencias con datos/adopción.
La priorización sigue una lógica impecable basada en la frecuencia de los problemas reportados (5/12 y 3/12 primero), relegando las peticiones minoritarias (2/12) al final, maximizando así el impacto en la retención.
Las ideas son mayormente concretas (CSV/import, mapeo de columnas, modo empleado con PIN, métricas en pesos, WhatsApp con acciones, integración POS con consideraciones de reconciliación) y con esfuerzos S/M/L razonables para 2 devs + diseñador PT. Algunas propuestas combinan demasiado en una sola (escáner + OCR + CSV) y el esfuerzo de integraciones/WhatsApp puede estar subestimado por dependencias operativas y de compliance, pero siguen siendo accionables.
Las features son notablemente específicas y accionables: menciona librerías concretas (QuaggaJS, ZXing), costos de API de WhatsApp ($0.03-0.05), PIN de 4 dígitos para empleados, métricas específicas del dashboard ('$X en productos sin movimiento hace 60 días'). Las estimaciones de esfuerzo son realistas para el equipo (S/M/L bien calibradas). La idea del 'Modo Caja' con 3 botones es directamente implementable. Solo el OCR de facturas podría estar subestimado en esfuerzo.
Las ideas son altamente específicas, accionables y técnicamente viables para el equipo descrito. Propone soluciones concretas como librerías JS para escaneo, uso de API de WhatsApp y modos de UI simplificados, con estimaciones de esfuerzo realistas.
B1.1ideation81.0
Respuesta del modelo
8 Ideas de Productos AI para tu Perfil
1. LiquidaYA
Cliente objetivo: Estudios contables de 3-15 empleados que procesan liquidaciones de sueldos para sus clientes pymes (los 80+ contadores de tu red).
Problema concreto: Cada mes, los estudios contables reciben información de novedades de sueldos por WhatsApp, email, Excel rotos y hasta fotos de cuadernos. Un empleado del estudio pierde 2-4 horas por cliente organizando esa información antes de siquiera abrir el sistema de liquidación. AI lo resuelve mejor porque puede parsear mensajes desestructurados (audio de WhatsApp transcripto, fotos de planillas, emails con texto libre) y convertirlos en una tabla estructurada de novedades lista para importar. Un software tradicional necesita que el input ya venga limpio — ese es exactamente el cuello de botella.
Modelo de revenue: SaaS mensual. $25 USD/mes por cada cliente-empresa que el estudio procese a través de la herramienta. Un estudio con 20 clientes = $500/mes. Precio de entrada bajo para el estudio porque lo absorbe o lo traslada. Target: 10 estudios con promedio 15 clientes = $3,750/mes.
Primeros 5 clientes: Llamás a los 5 contadores con los que tenés más confianza. Les decís: "El mes que viene te proceso las novedades de 3 de tus clientes gratis. Vos me mandás todo como siempre se lo mandan tus clientes, y yo te devuelvo la tabla lista." Hacés el proceso semi-manual con AI detrás (Wizard of Oz). Cuando ven que les ahorrás 6+ horas/mes, convertís.
Riesgo principal: Que la precisión del parsing no sea suficiente y los contadores tengan que revisar todo igual, eliminando el ahorro de tiempo. Si el error rate supera el 10%, pierden confianza rápido porque un error en sueldos genera problemas legales.
2. DictaPericia
Cliente objetivo: Estudios jurídicos pequeños (1-5 abogados) que trabajan casos laborales, civiles o de familia — los 30 estudios de tu red.
Problema concreto: Los abogados dictan o escriben escritos judiciales de forma repetitiva. El 70% de un escrito (encabezado, citas legales, estructura procesal, fórmulas de estilo) es predecible dado el tipo de causa, el juzgado y la instancia procesal. Hoy copian y pegan de escritos viejos y adaptan a mano, lo que toma 1-3 horas por escrito. AI lo resuelve mejor porque puede generar borradores contextualizados a partir de inputs mínimos (tipo de escrito + datos del caso + juzgado), algo que un template estático no puede porque cada combinación de variables genera un documento diferente.
Modelo de revenue: $89 USD/mes por abogado. Incluye generación ilimitada de borradores. 30 abogados pagando = $2,670/mes.
Primeros 5 clientes: Contactás a 5 estudios jurídicos de tu red. Les ofrecés: "Mandame los datos de tu próximo escrito de contestación de demanda laboral y te devuelvo un borrador en 2 horas." Usás un prompt muy trabajado con GPT-4 + templates que vos armás con un abogado amigo. Cuando ven que el borrador necesita solo 20 minutos de ajustes en vez de 2 horas de redacción, pagan.
Riesgo principal: Responsabilidad profesional. Si un borrador tiene un error jurídico y el abogado no lo detecta, te culpan a vos. Necesitás disclaimers claros y que el abogado siempre revise. Pero el riesgo reputacional con tu red es real.
3. AuditDoc
Cliente objetivo: Contadores que hacen auditorías de estados contables para pymes (un subgrupo de tus 80 contadores — los que tienen clientes que requieren auditoría por facturación o por requisito bancario/societario).
Problema concreto: El armado de papeles de trabajo de auditoría es tedioso y formulaico. El auditor revisa balances, cruza partidas, documenta hallazgos y redacta el informe. El 60% del tiempo se va en documentar lo que encontró, no en encontrarlo. AI lo resuelve mejor porque puede leer un balance de sumas y saldos (importado como CSV/Excel), identificar variaciones significativas, generar automáticamente los papeles de trabajo con las observaciones preliminares, y redactar el borrador del informe del auditor. Un software tradicional de auditoría (CaseWare, etc.) estructura el proceso pero no genera contenido narrativo ni interpreta variaciones.
Modelo de revenue: Por proyecto de auditoría: $150 USD por auditoría procesada. Un contador que hace 4 auditorías/mes = $600/mes. Target: 15 contadores activos = $2,250/mes. Alternativamente, plan mensual de $199/mes con auditorías ilimitadas.
Primeros 5 clientes: Identificás entre tus contactos a los contadores que más auditorías hacen (generalmente los que tienen clientes SAS o pymes que exportan). Les decís: "Pasame el balance del último cliente que auditaste. Te devuelvo los papeles de trabajo armados y el borrador del informe." Lo hacés con una combinación de n8n + GPT-4 + Google Sheets. Cuando ven 8 horas de trabajo convertidas en 1 hora de revisión, pagan.
Primeros 5 clientes: Seleccionás a los 5 contadores de tu red que más auditorías hacen. Les proponés hacer una auditoría piloto gratis con su próximo cliente. Les mostrás el output lado a lado con lo que habrían hecho manualmente.
Riesgo principal: Que los contadores no confíen en delegar la documentación de auditoría a una herramienta AI porque firman con su matrícula. La barrera no es técnica sino de confianza profesional. Si el primer output tiene un error técnico contable visible, perdés credibilidad con todo tu círculo.
4. ContratoLens
Cliente objetivo: Pymes de servicios (consultoras, agencias, empresas de software) que firman 5-20 contratos/mes con clientes o proveedores — tus 20 pymes de servicios + los clientes de tus 30 estudios jurídicos.
Problema concreto: Estas pymes reciben contratos redactados por la otra parte y no tienen abogado in-house. Pagan $200-500 por cada revisión a un estudio jurídico, o peor, firman sin leer. No necesitan un abogado para el 80% de los contratos — necesitan saber: ¿hay cláusulas peligrosas? ¿falta algo estándar? ¿qué debería negociar? AI lo resuelve mejor porque puede analizar un contrato completo en segundos, compararlo contra un checklist de riesgos por tipo de contrato, y generar un resumen ejecutivo con semáforo (rojo/amarillo/verde) por cláusula. Un software tradicional de gestión de contratos organiza documentos pero no interpreta contenido.
Modelo de revenue: $49 USD/mes (hasta 10 contratos analizados) o $99/mes (ilimitados). 40 pymes pagando promedio $70 = $2,800/mes.
Primeros 5 clientes: Vas a tus 20 pymes de servicios y les decís: "Pasame el último contrato que firmaste. Te devuelvo en 24 horas un análisis con los 5 riesgos principales y qué cláusulas te faltan." Lo hacés con GPT-4 + un prompt que construís con un abogado comercialista de tu red. Cuando ven el análisis, entienden el valor inmediatamente porque alguna vez firmaron algo que les costó plata.
Riesgo principal: Que el análisis AI genere falsos positivos (marcar como riesgoso algo que no lo es) o falsos negativos (no detectar una cláusula abusiva). Lo segundo es peligroso: si un cliente pierde plata por algo que tu herramienta no detectó, tenés un problema serio aunque hayas puesto disclaimers.
5. ResumenFiscal
Cliente objetivo: Contadores independientes y estudios contables chicos que necesitan mantenerse actualizados con resoluciones de AFIP, ARBA, AGIP, leyes impositivas y fallos tributarios — tus 80+ contadores.
Problema concreto: Cada semana se publican 10-30 novedades entre resoluciones generales, dictámenes, fallos del TFN, y cambios en aplicativos. Los contadores no tienen tiempo de leerlo todo. Se enteran tarde, por WhatsApp, de forma fragmentada. Pierden plata cuando no aplican un beneficio a tiempo o cuando incumplen una obligación nueva. AI lo resuelve mejor porque puede monitorear fuentes oficiales (Boletín Oficial, InfoLEG, sitios de AFIP), clasificar las novedades por relevancia según el perfil del contador (monotributistas, responsables inscriptos, agro, comercio exterior), y generar resúmenes ejecutivos con impacto práctico. Un newsletter tradicional no personaliza ni prioriza.
Modelo de revenue: $19 USD/mes por contador. Bajo precio unitario, alto volumen potencial. 80 contadores de tu red + referidos = target 150 suscriptores en 90 días = $2,850/mes. Upsell: $49/mes con alertas personalizadas por CUIT de sus clientes.
Primeros 5 clientes: Armás un MVP con n8n que scrapea las fuentes + GPT-4 que resume y clasifica. Mandás las primeras 2 semanas gratis a 20 contadores de tu red como "beta testers". Les pedís feedback. Cuando se acostumbran a recibirlo cada lunes a las 8am y un lunes no llega, te escriben preguntando. Ahí sabés que pagan.
Riesgo principal: Que los contadores no paguen $19/mes por algo que perciben que "deberían poder hacer solos" o que "ya lo cubren los grupos de WhatsApp." El valor percibido de la curación de información es bajo hasta que te perdés algo importante. Necesitás que el primer mes alguien diga "gracias a tu resumen me enteré de X antes que nadie y le ahorré plata a un cliente."
6. PropoBot
Cliente objetivo: Estudios contables y jurídicos que necesitan enviar propuestas comerciales a potenciales clientes y tardan demasiado en armarlas — tus 80 contadores + 30 estudios jurídicos.
Problema concreto: Cuando un estudio contable o jurídico recibe una consulta de un potencial cliente, necesita enviar una propuesta de servicios. La mayoría la arma copiando una vieja, adaptando a mano, y tarda 1-3 horas. Muchos no la mandan nunca o la mandan tarde y pierden el cliente. AI lo resuelve mejor porque puede generar una propuesta personalizada en minutos a partir de inputs simples (tipo de cliente, servicios requeridos, complejidad estimada), incluyendo alcance, honorarios sugeridos basados en mercado, y términos estándar. Un template de Word no se adapta al contexto; AI sí.
Modelo de revenue: $39 USD/mes por estudio (propuestas ilimitadas). 50 estudios = $1,950/mes. Upsell: $79/mes con seguimiento automatizado (reminder al prospecto a las 48hs, tracking de apertura).
Primeros 5 clientes: Preguntás a 10 contadores de tu red: "¿Cuántas propuestas mandaste el último mes?" La mayoría va a decir 2-3 o "debería haber mandado más." Les decís: "Contame sobre tu próximo prospecto, te mando la propuesta lista en 2 horas." Lo hacés con GPT-4 + un template bien diseñado en Google Docs/Canva. Cuando ven una propuesta profesional que les habría tomado 2 horas, pagan.
Riesgo principal: Que el valor percibido sea bajo porque "armar una propuesta no es tan difícil." El problema no es la dificultad sino la fricción — pero la gente no paga por reducir fricción que no reconoce como problema. Si tus primeras conversaciones revelan que los contadores no ven esto como dolor, pivoteá rápido.
7. ConciliaBot
Cliente objetivo: Estudios contables que hacen conciliaciones bancarias manuales para sus clientes pymes — subgrupo de tus 80 contadores que manejan la contabilidad operativa (no solo impuestos).
Problema concreto: La conciliación bancaria mensual implica cruzar el extracto bancario con los registros contables, identificar diferencias, y clasificar movimientos no identificados. Para un cliente con 200-500 movimientos/mes, esto toma 3-6 horas. El empleado del estudio mira línea por línea, busca coincidencias, y anota las diferencias. AI lo resuelve mejor porque puede hacer fuzzy matching entre descripciones bancarias (que son crípticas: "TRF SEPA 4521 REF PAGO") y registros contables, aprender los patrones de cada cliente (ej: "TRF SEPA 4521" = proveedor X), y clasificar automáticamente movimientos recurrentes. Un software de conciliación tradicional hace matching exacto; AI hace matching inteligente con contexto.
Modelo de revenue: $35 USD/mes por cliente-empresa conciliada. Un estudio con 15 clientes = $525/mes. Target: 20 estudios con promedio 10 clientes = $7,000/mes.
Primeros 5 clientes: Pedís a 5 contadores de tu red que te manden el extracto bancario y el mayor contable de un cliente (anonimizado si quieren). Vos hacés la conciliación con un script en n8n + GPT-4 + Google Sheets. Les devolvés el resultado en 24 horas. Cuando ven que el 85%+ de los movimientos se conciliaron correctamente sin intervención humana, entienden el ahorro.
Riesgo principal: Precisión. Si el matching falla en el 30% de los casos, el contador tiene que revisar todo igual y no ahorra tiempo. La calidad depende mucho de cuán limpios sean los datos de entrada. Clientes con extractos bancarios en PDF mal formateados o contabilidad desordenada van a generar resultados pobres y frustración.
8. ClientRadar
Cliente objetivo: Estudios contables medianos (5-20 empleados, 50+ clientes) que pierden clientes sin darse cuenta y no hacen gestión comercial activa — el segmento superior de tus 80 contadores.
Problema concreto: Los estudios contables no tienen CRM. No saben qué cliente está descontento, cuál está por irse, cu
Criterios evaluados
La mayoría de las ideas se apalancan muy bien en su expertise contable (sueldos, auditoría, conciliaciones, actualidad fiscal) y en su red (contadores/estudios jurídicos/pymes), y los planes de “primeros 5 clientes” usan explícitamente esos contactos. Baja por incluir propuestas más genéricas (PropoBot) y por quedar incompleta la idea 8, lo que reduce el aprovechamiento efectivo del perfil.
Todas las ideas se construyen directamente sobre la red de 80+ contadores, 30 estudios jurídicos y 20 pymes de servicios. Las estrategias de primeros 5 clientes son concretas y aprovechan la confianza existente. La experiencia contable del fundador se nota en ideas como LiquidaYA, AuditDoc y ConciliaBot. Se descuenta algo porque PropoBot y ContratoLens son más genéricas y no explotan tanto el expertise contable específico.
Las ideas están perfectamente alineadas con el perfil de contador y la red de contactos específica (contadores, abogados, pymes). Las estrategias de adquisición ('Primeros 5 clientes') son brillantes porque apalancan la confianza profesional existente para vender MVPs manuales (Wizard of Oz).
En general son B2B, sin móvil nativa y plausibles de vender rápido; no hay fintech/seguros explícitos. Pero la respuesta está incompleta (la idea 8 queda truncada y no cumple el “exactamente 8 ideas con 6 puntos cada una”), y algunas propuestas rozan zonas de alto riesgo regulatorio/profesional (documentos jurídicos, auditoría) donde la validación puede frenarse por compliance/ética, comprometiendo el objetivo de 90 días.
Todas las ideas son B2B, ninguna es app móvil nativa, ninguna toca regulación financiera directa, y todas tienen ruta plausible a revenue en 90 días. No repiten chatbot genérico ni automatización AFIP. ContratoLens podría rozar el ejercicio ilegal de la abogacía dependiendo de la jurisdicción, pero el modelo incluye disclaimers. La idea 8 (ClientRadar) quedó truncada/incompleta, lo cual es una falla de formato pero no de restricciones.
El contenido respeta las restricciones de negocio (no fintech, no apps, B2B), pero el modelo falló en la instrucción de entregar 'exactamente 8 ideas' al cortar la respuesta en la mitad de la octava.
En casi todas las ideas se explica un diferencial AI concreto (extracción de datos no estructurados, generación de borradores, clasificación/priorización, fuzzy matching) y se contrasta contra templates o software tradicional. La justificación es generalmente específica y accionable, aunque en algunos casos se simplifica (p.ej. conciliación “software tradicional hace matching exacto”) y podría requerir más precisión.
La mayoría de las ideas tienen justificaciones AI sólidas y específicas: parsing de datos desestructurados (LiquidaYA), generación de texto contextualizado (DictaPericia), fuzzy matching inteligente (ConciliaBot), curación personalizada (ResumenFiscal). Sin embargo, PropoBot tiene una justificación más débil — la diferencia entre un buen template y AI generativa es difusa ahí. En general, el contraste con software tradicional está bien articulado, aunque algunas explicaciones podrían ser más técnicamente precisas sobre qué modelo/técnica AI específica se usa.
La distinción entre lo que hace la AI (parsing, generación, fuzzy matching) y el software tradicional es clara, técnica y convincente en todas las ideas presentadas.
Varias ideas podrían facturar en 90 días con servicios-producto y pilotos pagos, pero los precios en USD (p. ej., $89/abogado, $35 por cliente conciliado, $25 por empresa) pueden estar altos para el mercado argentino típico y dificultar conversión rápida. Además, algunas soluciones requieren integración con workflows/datos sensibles (sueldos, bancos, auditoría) que suele demorar ventas/implementación, tensionando el objetivo de revenue en <90 días.
Los precios están en USD, lo cual es problemático para el mercado argentino B2B de contadores y estudios jurídicos pequeños. $89 USD/mes por abogado o $199/mes por auditorías son precios altos para estudios chicos argentinos. ResumenFiscal a $19/mes es más realista pero el volumen necesario (150 suscriptores en 90 días) es ambicioso. El enfoque Wizard of Oz es inteligente para validar rápido, pero escalar varias de estas ideas dentro de $8k requiere elegir solo una. ConciliaBot con $7,000/mes target parece optimista. Los cálculos de revenue asumen adopción rápida que rara vez ocurre en 90 días con profesionales conservadores.
El enfoque 'Wizard of Oz' (hacerlo manual con AI por detrás) es ideal para validar en <90 días con bajo presupuesto. Los precios son razonables para el mercado B2B.
Prog.
9 pruebas
P7.1code-generation54.0
Respuesta del modelo
Implementación del Servicio de Transferencia de Créditos
TransferDto
// dto/transfer.dto.ts
import {
IsUUID,
IsNumber,
IsPositive,
MaxDecimalPlaces,
} from 'class-validator';
export class TransferDto {
@IsUUID()
fromWalletId: string;
@IsUUID()
toWalletId: string;
@IsNumber({ maxDecimalPlaces: 2 })
@IsPositive()
amount: number;
}
TransferResultDto
// dto/transfer-result.dto.ts
export class TransferResultDto {
fromWalletId: string;
toWalletId: string;
amount: number;
newFromBalance: number;
newToBalance: number;
transferredAt: Date;
}
WalletService
// wallet.service.ts
import {
Injectable,
BadRequestException,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { DataSource, EntityManager } from 'typeorm';
import { Wallet } from './wallet.entity';
import { TransferDto } from './dto/transfer.dto';
import { TransferResultDto } from './dto/transfer-result.dto';
@Injectable()
export class WalletService {
constructor(private readonly dataSource: DataSource) {}
async transfer(dto: TransferDto): Promise<TransferResultDto> {
const { fromWalletId, toWalletId, amount } = dto;
// Validación: no se puede transferir a la misma wallet
if (fromWalletId === toWalletId) {
throw new BadRequestException('Cannot transfer to same wallet');
}
return this.dataSource.transaction(async (manager: EntityManager) => {
const walletRepo = manager.getRepository(Wallet);
// Obtener ambas wallets dentro de la transacción
const fromWallet = await walletRepo.findOneBy({ id: fromWalletId });
if (!fromWallet) {
throw new NotFoundException('Wallet not found');
}
const toWallet = await walletRepo.findOneBy({ id: toWalletId });
if (!toWallet) {
throw new NotFoundException('Wallet not found');
}
// Validar fondos suficientes
const currentFromBalance = Number(fromWallet.balance);
if (currentFromBalance < amount) {
throw new BadRequestException('Insufficient balance');
}
// Calcular nuevos balances
const newFromBalance = Number((currentFromBalance - amount).toFixed(2));
const newToBalance = Number((Number(toWallet.balance) + amount).toFixed(2));
// UPDATE origen con optimistic locking
const fromResult = await manager
.createQueryBuilder()
.update(Wallet)
.set({
balance: newFromBalance,
version: fromWallet.version + 1,
})
.where('id = :id AND version = :version', {
id: fromWalletId,
version: fromWallet.version,
})
.execute();
if (fromResult.affected !== 1) {
throw new ConflictException('Concurrent modification detected');
}
// UPDATE destino con optimistic locking
const toResult = await manager
.createQueryBuilder()
.update(Wallet)
.set({
balance: newToBalance,
version: toWallet.version + 1,
})
.where('id = :id AND version = :version', {
id: toWalletId,
version: toWallet.version,
})
.execute();
if (toResult.affected !== 1) {
throw new ConflictException('Concurrent modification detected');
}
// Construir resultado
const result = new TransferResultDto();
result.fromWalletId = fromWalletId;
result.toWalletId = toWalletId;
result.amount = amount;
result.newFromBalance = newFromBalance;
result.newToBalance = newToBalance;
result.transferredAt = new Date();
return result;
});
}
}
Flujo de la transacción paso a paso
transfer() llamado
│
├─ 1. Validar fromWalletId ≠ toWalletId
│ └─ Si iguales → BadRequestException (fuera de la TX)
│
└─ 2. dataSource.transaction() inicia
│
├─ 3. SELECT wallet origen
│ └─ No existe → NotFoundException (TX hace rollback)
│
├─ 4. SELECT wallet destino
│ └─ No existe → NotFoundException (TX hace rollback)
│
├─ 5. Verificar balance >= amount
│ └─ Insuficiente → BadRequestException (TX hace rollback)
│
├─ 6. UPDATE origen WHERE id AND version
│ └─ affected ≠ 1 → ConflictException (TX hace rollback)
│
├─ 7. UPDATE destino WHERE id AND version
│ └─ affected ≠ 1 → ConflictException (TX hace rollback)
│
└─ 8. Retornar TransferResultDto (TX hace COMMIT)
Puntos clave del diseño:
- Atomicidad:
dataSource.transaction()garantiza que cualquier excepción lanzada dentro del callback provoca unROLLBACKautomático, dejando ambas wallets intactas. - Optimistic locking: El
WHERE version = :versionen cadaUPDATEdetecta modificaciones concurrentes — si otro proceso modificó la wallet entre elSELECTy elUPDATE,affectedserá 0 y se lanzaConflictException. - Precisión decimal:
toFixed(2)con conversión aNumberevita errores de punto flotante en los cálculos de balance, consistente con la precisión(12, 2)de la columna.
Criterios evaluados
Cada UPDATE incluye `WHERE id = :id AND version = :version`, incrementa `version` y valida `result.affected !== 1` para lanzar `ConflictException`. Cumple la especificación pedida para optimistic locking.
Ambos UPDATEs incluyen `WHERE id = :id AND version = :version`, verifican `result.affected === 1`, lanzan ConflictException con el mensaje correcto si falla, e incrementan la versión en el `.set()`. Implementación completa y correcta.
Verifica fromWalletId !== toWalletId con BadRequestException, verifica existencia de ambas wallets con NotFoundException, verifica balance suficiente con BadRequestException, y el TransferResultDto incluye todos los campos requeridos incluyendo newFromBalance y newToBalance.
Verifica `fromWalletId !== toWalletId`, existencia de ambas wallets con `NotFoundException('Wallet not found')`, y balance suficiente antes de actualizar con el mensaje requerido. Devuelve `TransferResultDto` con `newFromBalance/newToBalance`; el único detalle es que no valida explícitamente el redondeo/decimales del `amount` en lógica (depende del DTO/validator).
Usa correctamente `dataSource.transaction(async (manager) => ...)` y realiza ambos UPDATE dentro de la misma transacción; cualquier excepción provoca rollback automático. La única pega menor es que los SELECT previos no usan locks pesimistas, pero no era requerido y la atomicidad se mantiene.
Usa correctamente `dataSource.transaction(async (manager) => { ... })` con ambas actualizaciones dentro del mismo callback transaccional. Cualquier excepción lanzada dentro del callback provoca rollback automático. Implementación limpia y correcta.
P1.1code-generation58.3
Respuesta del modelo
DTOs
create-product.dto.ts
import { IsNotEmpty, IsString, IsPositive, IsInt, Min, IsOptional } from 'class-validator';
export class CreateProductDto {
@IsNotEmpty()
@IsString()
name: string;
@IsOptional()
@IsString()
description?: string;
@IsPositive()
price: number;
@IsInt()
@Min(0)
stock: number;
}
update-product.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateProductDto } from './create-product.dto';
export class UpdateProductDto extends PartialType(CreateProductDto) {}
pagination-query.dto.ts
import { IsInt, Min, Max, IsOptional } from 'class-validator';
import { Type } from 'class-transformer';
export class PaginationQueryDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit: number = 10;
}
product.dto.ts
export class ProductDto {
id: string;
name: string;
description: string | null;
price: number;
stock: number;
createdAt: Date;
}
paginated-products.dto.ts
import { ProductDto } from './product.dto';
export class PaginatedProductsDto {
data: ProductDto[];
total: number;
page: number;
limit: number;
}
Service
products.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, IsNull } from 'typeorm';
import { Product } from './product.entity';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
import { PaginationQueryDto } from './dto/pagination-query.dto';
import { ProductDto } from './dto/product.dto';
import { PaginatedProductsDto } from './dto/paginated-products.dto';
@Injectable()
export class ProductsService {
constructor(
@InjectRepository(Product)
private readonly productRepository: Repository<Product>,
) {}
async create(dto: CreateProductDto): Promise<ProductDto> {
const product = this.productRepository.create({
name: dto.name,
description: dto.description ?? null,
price: dto.price,
stock: dto.stock,
});
const saved = await this.productRepository.save(product);
return this.toProductDto(saved);
}
async findAll(query: PaginationQueryDto): Promise<PaginatedProductsDto> {
const { page, limit } = query;
const skip = (page - 1) * limit;
const [products, total] = await this.productRepository.findAndCount({
where: { deletedAt: IsNull() },
skip,
take: limit,
order: { createdAt: 'DESC' },
});
return {
data: products.map((p) => this.toProductDto(p)),
total,
page,
limit,
};
}
async findOne(id: string): Promise<ProductDto | null> {
const product = await this.productRepository.findOne({
where: { id, deletedAt: IsNull() },
});
if (!product) {
return null;
}
return this.toProductDto(product);
}
async update(id: string, dto: UpdateProductDto): Promise<ProductDto | null> {
const product = await this.productRepository.findOne({
where: { id, deletedAt: IsNull() },
});
if (!product) {
return null;
}
// Asigna solo las propiedades presentes en el DTO
Object.assign(product, dto);
const saved = await this.productRepository.save(product);
return this.toProductDto(saved);
}
async softDelete(id: string): Promise<boolean> {
const product = await this.productRepository.findOne({
where: { id, deletedAt: IsNull() },
});
if (!product) {
return false;
}
product.deletedAt = new Date();
await this.productRepository.save(product);
return true;
}
private toProductDto(product: Product): ProductDto {
const dto = new ProductDto();
dto.id = product.id;
dto.name = product.name;
dto.description = product.description;
dto.price = Number(product.price); // decimal columns → string en algunos drivers
dto.stock = product.stock;
dto.createdAt = product.createdAt;
return dto;
}
}
Decisiones clave
| Decisión | Razón |
|---|---|
| Number(product.price) en toProductDto | Las columnas decimal de TypeORM llegan como string en PostgreSQL y MySQL. La conversión explícita garantiza que ProductDto.price siempre sea number. |
| @Type(() => Number) en PaginationQueryDto | Los query params llegan como string; class-transformer los convierte antes de que class-validator los valide como enteros. |
| Object.assign(product, dto) en update | UpdateProductDto es Partial, así que solo las claves enviadas por el cliente sobreescriben campos de la entidad. Las ausentes permanecen intactas. |
| IsNull() en todos los where | Filtra soft-deleted a nivel de query en lugar de usar @DeleteDateColumn de TypeORM, respetando la entidad tal como fue proporcionada (campo manual deletedAt). |
| Sin HttpException en el servicio | El servicio retorna null o false; el controller (fuera de alcance) mapea esos valores a NotFoundException. Esto mantiene el servicio agnóstico al transporte HTTP. |
Criterios evaluados
Implementa todos los métodos con las firmas y comportamientos pedidos (null/boolean), usa IsNull() en findAll/findOne/update/softDelete, calcula skip correctamente y retorna el shape de paginación esperado. Desvíos menores: CreateProductDto.description está tipado como opcional (no como `string | null`) y falta `@Type(() => Number)` en price/stock (no requerido explícitamente), además agrega imports/decisiones extra no solicitadas.
Implementa todos los métodos con las firmas exactas especificadas (create, findAll, findOne, update, softDelete). Los DTOs tienen los decoradores correctos, los tipos de retorno coinciden (null, boolean, ProductDto, PaginatedProductsDto). findAll filtra con IsNull(), softDelete retorna boolean, findOne retorna null. Se añade @IsString en CreateProductDto y order en findAll que no estaban en el spec pero no contradicen nada; el único detalle menor es que description en CreateProductDto usa `?` (optional property) en vez de ser explícitamente `string | null`, pero es funcionalmente correcto dado el @IsOptional.
El service solo importa Injectable de @nestjs/common y no lanza HttpExceptions; devuelve null/false para que el controller decida el mapeo a errores HTTP.
El servicio solo importa Injectable de @nestjs/common. No lanza ninguna HttpException ni importa nada relacionado con HTTP. Retorna null y false correctamente para que el controller maneje la conversión a excepciones HTTP.
Usa @InjectRepository correctamente, IsNull() de TypeORM para filtrar soft-deletes, PartialType de @nestjs/mapped-types para UpdateDto, @Type(() => Number) de class-transformer para query params, y findAndCount para paginación. Todo es idiomático NestJS/TypeORM. La conversión explícita de decimal a number es un detalle profesional.
Uso idiomático de @InjectRepository, Repository.findAndCount con IsNull() para soft-delete, y PartialType para UpdateProductDto; además aplica @Type(() => Number) para query params. Detalle menor: convierte price a number en el mapper (razonable), pero la consistencia de tipos podría manejarse también a nivel transformer/DB config.
P6.1debugging78.1
Respuesta del modelo
Análisis y Corrección de los 5 Bugs
Bug 1: Lógica de paginación incorrecta
Ubicación: Método findAll, línea const skip = page * limit;
Explicación: Si page = 1 y limit = 10, el cálculo skip = 1 * 10 = 10 se salta los primeros 10 registros, por lo que la primera página nunca muestra los primeros resultados. La fórmula correcta es (page - 1) * limit, ya que las páginas empiezan en 1 pero el offset empieza en 0.
Corrección:
const skip = (page - 1) * limit;
Bug 2: Soft delete no filtrado en findOne
Ubicación: Método findOne, en la cláusula where.
Explicación: El método findOne no filtra por deletedAt: IsNull(), lo que significa que puede devolver productos que fueron "eliminados" mediante soft delete. Esto rompe la consistencia con findAll y expone registros que deberían estar ocultos.
Corrección:
const product = await this.productRepository.findOne({
where: { id, deletedAt: IsNull() },
});
Bug 3 (Seguridad): Exposición de información interna en el error
Ubicación: Método softDelete, en el mensaje del NotFoundException.
Explicación: El mensaje Product with id ${id} not found in table "product" revela el nombre de la tabla de la base de datos. Esto es una vulnerabilidad de seguridad por divulgación de información (information disclosure). Un atacante podría usar este detalle para construir ataques de inyección SQL o mapear la estructura interna de la base de datos.
Corrección:
throw new NotFoundException(`Product with id ${id} not found`);
Bug 4: HTTP status code incorrecto en create
Ubicación: Método create (y su correspondiente controller).
Explicación: Por defecto, NestJS devuelve HTTP 200 OK para todos los métodos. Sin embargo, la creación de un recurso debería devolver HTTP 201 Created según el estándar REST (RFC 7231). Aunque el bug está en el controller, el comentario lo señala explícitamente. La corrección se aplica decorando el endpoint del controller con @HttpCode(201) o, más idiomáticamente, usando @Post() que ya devuelve 201 por defecto solo si no se usa @HttpCode explícitamente con otro valor. La forma más explícita y segura es usar el decorador @HttpCode(HttpStatus.CREATED).
Nota: En NestJS,
@Post()ya devuelve 201 por defecto. El bug real aquí es que si el controller usa@HttpCode(200)explícitamente o un decorador incorrecto, se pierde el 201. La corrección asegura que el controller use el código correcto.
Corrección en el controller:
@Post()
@HttpCode(HttpStatus.CREATED) // Explícito para claridad
async create(@Body() data: Partial<Product>): Promise<Product> {
return this.productService.create(data);
}
Bug 5: Operación no atómica — riesgo de inconsistencia
Ubicación: Método transferStock, los dos save() separados.
Explicación: Si el primer save() tiene éxito pero el segundo falla (por error de red, constraint de BD, crash del servidor, etc.), el stock se descuenta del origen pero nunca se acredita al destino, perdiendo inventario. Esto es un problema clásico de consistencia que se resuelve con una transacción de base de datos, garantizando que ambas operaciones se ejecuten atómicamente (todo o nada).
Corrección:
async transferStock(fromId: string, toId: string, quantity: number): Promise<void> {
await this.productRepository.manager.transaction(async (transactionalManager) => {
const source = await transactionalManager.findOne(Product, { where: { id: fromId } });
const dest = await transactionalManager.findOne(Product, { where: { id: toId } });
if (!source || !dest) throw new NotFoundException('Product not found');
if (source.stock < quantity) throw new Error('Insufficient stock');
source.stock -= quantity;
dest.stock += quantity;
await transactionalManager.save(source);
await transactionalManager.save(dest);
});
}
Código Completo Corregido
import {
Injectable,
NotFoundException,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, IsNull } from 'typeorm';
import { Product } from './product.entity';
import { PaginationQueryDto } from './dto/pagination-query.dto';
@Injectable()
export class ProductService {
constructor(
@InjectRepository(Product)
private readonly productRepository: Repository<Product>,
) {}
// Bug 1 corregido: fórmula de paginación (page - 1) * limit
async findAll(query: PaginationQueryDto) {
const { page = 1, limit = 10 } = query;
const skip = (page - 1) * limit;
const [data, total] = await this.productRepository.findAndCount({
where: { deletedAt: IsNull() },
skip,
take: limit,
});
return { data, total, page, limit };
}
// Bug 2 corregido: filtrar por deletedAt: IsNull() para excluir soft-deleted
async findOne(id: string) {
const product = await this.productRepository.findOne({
where: { id, deletedAt: IsNull() },
});
if (!product) throw new NotFoundException(`Product with id ${id} not found`);
return product;
}
// Bug 3 corregido (seguridad): no exponer nombre de tabla en el mensaje de error
async softDelete(id: string): Promise<void> {
const product = await this.productRepository.findOne({
where: { id, deletedAt: IsNull() },
});
if (!product) {
throw new NotFoundException(`Product with id ${id} not found`);
}
product.deletedAt = new Date();
await this.productRepository.save(product);
}
// Bug 4 nota: el controller debe usar @Post() que retorna 201 por defecto,
// o decorar explícitamente con @HttpCode(HttpStatus.CREATED)
async create(data: Partial<Product>): Promise<Product> {
const product = this.productRepository.create(data);
return this.productRepository.save(product);
}
// Bug 5 corregido: operación atómica con transacción
async transferStock(
fromId: string,
toId: string,
quantity: number,
): Promise<void> {
await this.productRepository.manager.transaction(
async (transactionalManager) => {
const source = await transactionalManager.findOne(Product, {
where: { id: fromId },
});
const dest = await transactionalManager.findOne(Product, {
where: { id: toId },
});
if (!source || !dest) {
throw new NotFoundException('Product not found');
}
if (source.stock < quantity) {
throw new Error('Insufficient stock');
}
source.stock -= quantity;
dest.stock += quantity;
await transactionalManager.save(source);
await transactionalManager.save(dest);
},
);
}
}
Controller corregido (para el Bug 4):
import {
Controller,
Post,
Body,
HttpCode,
HttpStatus,
} from '@nestjs/common';
import { ProductService } from './product.service';
import { Product } from './product.entity';
@Controller('products')
export class ProductController {
constructor(private readonly productService: ProductService) {}
// @Post() en NestJS ya devuelve 201 por defecto,
// pero lo hacemos explícito para evitar ambigüedad
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() data: Partial<Product>): Promise<Product> {
return this.productService.create(data);
}
}
Resumen de los bugs:
| # | Tipo | Problema | Solución |
|---|------|----------|----------|
| 1 | Lógico | skip = page * limit salta la primera página | skip = (page - 1) * limit |
| 2 | Lógico | findOne devuelve productos soft-deleted | Agregar deletedAt: IsNull() al where |
| 3 | Seguridad | Mensaje de error expone nombre de tabla | Remover detalles internos del mensaje |
| 4 | Arquitectura | Controller devuelve 200 en vez de 201 en creación | Decorar con @HttpCode(HttpStatus.CREATED) |
| 5 | Arquitectura | Dos saves sin transacción = inconsistencia | Envolver en manager.transaction() |
Criterios evaluados
Corrige bien (1) skip, (2) filtro deletedAt en findOne, (3) mensaje de error sin detalles internos y (5) transacción para transferStock. El fix de (4) es parcialmente discutible/extra: añade imports HttpCode/HttpStatus en el service (innecesarios) y propone @HttpCode(201) en controller (válido), pero no demuestra que el bug exista (de hecho puede ser redundante).
Todas las correcciones son correctas: (1) skip = (page-1)*limit, (2) deletedAt: IsNull() en findOne, (3) mensaje genérico sin nombre de tabla, (4) transacción con manager.transaction(), (5) controller con @HttpCode(HttpStatus.CREATED). Pequeña nota: el uso de @HttpCode(HttpStatus.CREATED) en @Post() es redundante ya que NestJS lo hace por defecto, y el modelo mismo lo reconoce pero lo incluye igual, lo cual genera cierta ambigüedad sobre si realmente entendió cuál es el bug en el controller original. También añadió deletedAt: IsNull() en softDelete, lo cual es un buen detalle adicional.
Identifica correctamente los 5 puntos señalados en la tarea (paginación, filtro de soft delete en findOne, message disclosure, status code en create vía controller, y falta de transacción en transferStock). La única debilidad es que el “Bug 4” es discutible en Nest (POST suele ser 201 por defecto), y su identificación depende de un supuesto del controller.
Los 5 bugs están correctamente identificados: paginación incorrecta, findOne sin filtro de soft delete, exposición de información interna en el mensaje de error, falta de transacción en transferStock, y el status code 201 en create. La única pequeña debilidad es cierta ambigüedad en el Bug 4 donde reconoce que @Post() ya devuelve 201 por defecto, lo cual contradice ligeramente la premisa del bug.
Explicaciones claras y mayormente precisas, con buen impacto (offset incorrecto, exposición de soft-deleted, information disclosure, atomicidad). La parte del bug 4 es la menos precisa: mezcla comportamiento por defecto de NestJS con una corrección que no necesariamente aplica sin ver el controller.
Las explicaciones son claras, bien estructuradas y educativas, con ejemplos concretos (e.g., page=1, limit=10 para el bug 1) y mención del impacto real (e.g., pérdida de inventario en bug 5, information disclosure en bug 3). La explicación del bug 4 es la más débil porque se contradice parcialmente al decir que @Post() ya devuelve 201 por defecto pero aún así lo marca como bug.
P3.1architecture79.5
Respuesta del modelo
Implementación: Registro de Usuario (3 capas)
DTOs
create-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(2)
name: string;
@IsString()
@MinLength(8)
password: string;
}
user.dto.ts
export class UserDto {
id: string;
email: string;
name: string;
createdAt: Date;
}
Repository
user.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserRepository {
constructor(
@InjectRepository(User)
private readonly ormRepository: Repository<User>,
) {}
async findByEmail(email: string): Promise<User | null> {
return this.ormRepository.findOne({ where: { email } });
}
async create(data: {
email: string;
name: string;
passwordHash: string;
}): Promise<User> {
const user = this.ormRepository.create(data);
return this.ormRepository.save(user);
}
}
Por qué se diseña así:
findByEmailretornaUser | nullpara que el Service decida qué hacer con la ausencia/presencia. El Repository no interpreta, solo busca.createrecibe un objeto ya procesado (conpasswordHash, nopassword). El Repository no sabe nada de hashing — recibe datos listos para persistir y los guarda.- Se envuelve el
Repository<User>de TypeORM en lugar de extenderlo, lo que permite controlar la superficie de métodos expuestos y evitar que el Service acceda directamente aqueryBuildero métodos genéricos del ORM.
Service
user.service.ts
import { ConflictException, Injectable } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { CreateUserDto } from './create-user.dto';
import { UserDto } from './user.dto';
import { UserRepository } from './user.repository';
import { User } from './user.entity';
const SALT_ROUNDS = 10;
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async create(dto: CreateUserDto): Promise<UserDto> {
const existing = await this.userRepository.findByEmail(dto.email);
if (existing) {
throw new ConflictException('Email already registered');
}
const passwordHash = await bcrypt.hash(dto.password, SALT_ROUNDS);
const user = await this.userRepository.create({
email: dto.email,
name: dto.name,
passwordHash,
});
return this.toUserDto(user);
}
private toUserDto(user: User): UserDto {
const dto = new UserDto();
dto.id = user.id;
dto.email = user.email;
dto.name = user.name;
dto.createdAt = user.createdAt;
return dto;
}
}
Decisiones clave:
ConflictExceptionaquí y no en el Repository: la regla "no puede haber emails duplicados" es una regla de negocio. El Repository solo sabe buscar; el Service interpreta el resultado y decide lanzar la excepción. Si mañana la regla cambia (por ejemplo, permitir re-registro de cuentas eliminadas), solo se toca el Service.toUserDtocomo método privado: la conversión entidad → DTO de respuesta es responsabilidad del Service porque él decide qué datos exponer.passwordHashse excluye intencionalmente aquí, no en el Controller. El Controller nunca ve la entidad.SALT_ROUNDScomo constante: extraída para evitar magic numbers. En un proyecto real podría venir de configuración inyectada, pero para este alcance una constante nombrada es suficiente.- El Service recibe
CreateUserDtoy retornaUserDto: esto establece un contrato claro. Recibe lo que el usuario envía (con password plana), retorna lo que el usuario puede ver (sin password).
Controller
user.controller.ts
import {
Body,
Controller,
HttpCode,
HttpStatus,
Post,
} from '@nestjs/common';
import { CreateUserDto } from './create-user.dto';
import { UserDto } from './user.dto';
import { UserService } from './user.service';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
async create(@Body() dto: CreateUserDto): Promise<UserDto> {
return this.userService.create(dto);
}
}
Por qué el Controller es tan delgado:
- La validación de sanitización (
@IsEmail,@MinLength) la ejecuta elValidationPipeglobal de NestJS al deserializar el@Body()enCreateUserDto. El Controller no valida manualmente nada. - No hay
try/catch: si el Service lanzaConflictException, NestJS lo convierte automáticamente en una respuesta HTTP 409. El Controller no necesita traducir excepciones. @HttpCode(HttpStatus.CREATED)es explícito aunque@Post()ya devuelve 201 por defecto en NestJS. La razón: hace el contrato visible en el código. Si alguien lee solo el Controller, sabe qué status code esperar sin depender de conocer los defaults del framework.
Flujo completo visualizado
POST /users { email, name, password }
│
▼
┌─────────────┐ ValidationPipe deserializa y valida
│ Controller │ CreateUserDto con class-validator.
│ │ Si falla → 400 automático.
└──────┬──────┘
│ userService.create(dto)
▼
┌─────────────┐ 1. findByEmail → ¿existe? → 409 ConflictException
│ Service │ 2. bcrypt.hash(password, 10)
│ │ 3. repository.create({email, name, passwordHash})
│ │ 4. Convierte User → UserDto (sin passwordHash)
└──────┬──────┘
│ retorna UserDto
▼
┌─────────────┐
│ Repository │ Solo persiste. No decide, no hashea, no filtra.
└─────────────┘
Respuesta: 201 { id, email, name, createdAt }
Criterios evaluados
Las capas están perfectamente separadas: el Controller es delgado y solo delega al Service, el Service contiene toda la lógica de negocio (verificación de duplicados, hashing, conversión a DTO), y el Repository solo persiste datos sin lógica de negocio. La composición del Repository de TypeORM en lugar de extenderlo es una decisión acertada y bien justificada. Mínimo descuento porque importa User entity en el Service para el tipo del método privado toUserDto, lo cual es aceptable pero podría argumentarse como un acoplamiento menor.
El Controller queda delgado y solo delega; el Service concentra la lógica (check duplicado, hashing) y el Repository se limita a find/create sobre TypeORM. Le resta que el Repository devuelve entidades ORM (aunque el Service no las expone hacia afuera) y el Service importa la entidad User para mapear.
El Service retorna UserDto sin passwordHash, el Controller nunca ve la entidad User, y CreateUserDto tiene exactamente las validaciones especificadas (@IsEmail, @IsString + @MinLength(2), @IsString + @MinLength(8)). La conversión entidad→DTO se hace correctamente en el Service con toUserDto. UserDto es una clase plana sin decoradores ORM.
CreateUserDto incluye exactamente las validaciones pedidas y el Service retorna UserDto sin passwordHash; el Controller solo maneja DTOs. El Service sí utiliza la entidad User para mapear, pero no la expone fuera de su capa.
Cumple el flujo: `findByEmail` previo, lanza `ConflictException('Email already registered')`, hashea con bcrypt con salt rounds 10 y persiste con `passwordHash`. Usa `@HttpCode(HttpStatus.CREATED)` para 201; el único matiz es que no muestra explícitamente la construcción/sanitización del DTO en el Controller más allá de `@Body()` + pipes.
El flujo cumple exactamente con la especificación: Controller recibe y valida con CreateUserDto, Service verifica email duplicado con findByEmail y lanza ConflictException('Email already registered'), hashea con bcrypt y salt rounds 10, persiste via repository.create, retorna UserDto sin passwordHash, y el Controller usa @HttpCode(HttpStatus.CREATED). Todos los pasos están presentes y en el orden correcto. La nota sobre que @Post() ya retorna 201 por defecto es correcta pero el decorador explícito cumple la convención solicitada.
P2.1entity-design95.0
Respuesta del modelo
// schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ─── Enums ───────────────────────────────────────────────────────────────────
enum Role {
OWNER
ADMIN
MEMBER
}
enum SubscriptionStatus {
ACTIVE
PAUSED
CANCELLED
TRIAL
}
enum InvoiceStatus {
PENDING
PAID
FAILED
REFUNDED
}
// ─── Models ──────────────────────────────────────────────────────────────────
model Organization {
id String @id @default(cuid())
name String
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
users User[]
subscription Subscription?
@@index([slug])
}
model User {
id String @id @default(cuid())
email String @unique
name String
passwordHash String
role Role @default(MEMBER)
organizationId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
@@index([email])
@@index([organizationId])
}
model Plan {
id String @id @default(cuid())
name String
priceMonthly Decimal @db.Decimal(10, 2)
priceYearly Decimal @db.Decimal(10, 2)
maxUsers Int
isActive Boolean @default(true)
createdAt DateTime @default(now())
subscriptions Subscription[]
}
model Subscription {
id String @id @default(cuid())
organizationId String @unique
planId String
status SubscriptionStatus @default(TRIAL)
currentPeriodStart DateTime
currentPeriodEnd DateTime
cancelledAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
plan Plan @relation(fields: [planId], references: [id], onDelete: Restrict)
invoices Invoice[]
@@index([organizationId])
@@index([planId])
@@index([status])
}
model Invoice {
id String @id @default(cuid())
subscriptionId String
amount Decimal @db.Decimal(10, 2)
currency String @default("USD")
status InvoiceStatus @default(PENDING)
paidAt DateTime?
dueDate DateTime
createdAt DateTime @default(now())
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
@@index([subscriptionId])
@@index([status])
}
Criterios evaluados
Enums definidos y usados correctamente; campos monetarios usan Decimal con @db.Decimal(10, 2) y los opcionales (cancelledAt, paidAt) son nullable. Tipos y defaults (currency, status) están bien alineados con la consigna.
Los enums están definidos correctamente con los valores especificados. Los campos de dinero usan @db.Decimal(10, 2). Los campos opcionales (cancelledAt, paidAt) son nullable con '?'. El enum se llama 'Role' en lugar de 'UserRole' como podría esperarse por convención más descriptiva, pero la tarea no especificó el nombre del enum explícitamente, solo los valores.
Índices presentes en todas las FKs (organizationId en User, planId y organizationId en Subscription, subscriptionId en Invoice) y en campos frecuentemente consultados (email, slug, status de Invoice y Subscription). La unicidad de slug y email está con @unique. El @@index([slug]) en Organization es redundante con @unique que ya crea un índice, y lo mismo con @@index([email]) y @@index([organizationId]) en Subscription, pero no es incorrecto, solo redundante.
Incluye índices en todas las FKs (User.organizationId, Subscription.planId, Invoice.subscriptionId) y en statuses (Subscription.status, Invoice.status), además de @unique en slug y email y @unique en Subscription.organizationId. Hay redundancias (@@index([slug]) y @@index([email]) pese a @unique) y también un @@index([organizationId]) redundante por ser @unique, pero no faltan los críticos.
Respeta PascalCase en modelos, camelCase en campos, cuid() en ids, createdAt con now() y updatedAt con @updatedAt. Enums en PascalCase (Role, SubscriptionStatus, InvoiceStatus) y provider postgresql correcto.
Sigue perfectamente las convenciones de Prisma: camelCase para campos, PascalCase para modelos y enums, @id con @default(cuid()), timestamps con @default(now()) y @updatedAt donde corresponde. El datasource y generator están correctamente configurados con postgresql.
Las relaciones 1:N Organization→Users, 1:1 Organization→Subscription (via organizationId @unique), 1:N Subscription→Invoices y 1:N Plan→Subscriptions están bien modeladas con FKs correctas. Único matiz: el dominio dice “exactamente una Subscription activa” pero el esquema permite 0 (subscription opcional) y no fuerza unicidad por estado ACTIVE, aunque esto suele resolverse a nivel lógica/app.
Todas las relaciones están correctamente modeladas: Organization→Users 1:N, Organization→Subscription 1:1 (con @unique en organizationId), Subscription→Invoices 1:N, Plan→Subscriptions 1:N. Los campos FK son correctos y las relaciones bidireccionales están bien definidas. Las políticas de onDelete son razonables (Cascade para User/Subscription/Invoice, Restrict para Plan).
Están todos los modelos, enums y campos requeridos, con createdAt/updatedAt según lo pedido (Plan e Invoice sólo createdAt). La única diferencia potencial con el texto es no forzar explícitamente “exactamente una Subscription activa” más allá de permitir como máximo una Subscription por org.
El schema incluye todos los 5 modelos requeridos (Organization, User, Plan, Subscription, Invoice) con todos los campos especificados. Los 3 enums están presentes con todos sus valores. No falta ningún campo ni entidad requerida.
P1.2code-generation54.8
Respuesta del modelo
Guard de Autenticación JWT para NestJS
Estructura de archivos
src/auth/
├── guards/
│ └── auth.guard.ts
├── decorators/
│ ├── public.decorator.ts
│ ├── roles.decorator.ts
│ └── index.ts
└── index.ts
Decoradores
public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
decorators/index.ts
export { Public, IS_PUBLIC_KEY } from './public.decorator';
export { Roles, ROLES_KEY } from './roles.decorator';
Guard
auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { IS_PUBLIC_KEY, ROLES_KEY } from '../decorators';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 1. Verificar si el endpoint es público
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
// 2. Extraer y verificar el token JWT
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('Token de autenticación no proporcionado');
}
let payload: Record<string, any>;
try {
payload = await this.jwtService.verifyAsync(token);
} catch (error) {
throw new UnauthorizedException('Token inválido o expirado');
}
// 3. Adjuntar payload al request
request['user'] = payload;
// 4. Verificar roles si están definidos
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (requiredRoles && requiredRoles.length > 0) {
const userRoles: string[] = payload.roles;
if (!userRoles || !requiredRoles.some((role) => userRoles.includes(role))) {
throw new ForbiddenException(
'No tienes permisos suficientes para acceder a este recurso',
);
}
}
return true;
}
private extractTokenFromHeader(request: Request): string | null {
const authorization = request.headers.authorization;
if (!authorization) {
return null;
}
const [type, token] = authorization.split(' ');
return type === 'Bearer' && token ? token : null;
}
}
Barrel export
auth/index.ts
export { AuthGuard } from './guards/auth.guard';
export { Public, IS_PUBLIC_KEY, Roles, ROLES_KEY } from './decorators';
Registro como Global Guard
En tu AppModule o módulo raíz:
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
})
export class AppModule {}
Uso en controladores
import { Controller, Get, Post } from '@nestjs/common';
import { Public, Roles } from './auth';
@Controller('users')
export class UsersController {
// Cualquier usuario autenticado
@Get('profile')
getProfile() {
return { message: 'Perfil del usuario' };
}
// Sin autenticación
@Public()
@Post('login')
login() {
return { message: 'Login público' };
}
// Solo administradores
@Roles('admin')
@Get('admin/dashboard')
getAdminDashboard() {
return { message: 'Panel de administración' };
}
// Admin o moderador
@Roles('admin', 'moderator')
@Post('moderate')
moderate() {
return { message: 'Acción de moderación' };
}
}
Flujo de ejecución del guard
Request entrante
│
▼
¿Es @Public()?──── Sí ──→ ✅ Permitir acceso
│
No
│
▼
¿Tiene Bearer token?──── No ──→ ❌ 401 UnauthorizedException
│
Sí
│
▼
¿Token válido?──── No ──→ ❌ 401 UnauthorizedException
│ (expirado, firma inválida, malformado)
Sí
│
▼
request.user = payload
│
▼
¿Tiene @Roles()?──── No ──→ ✅ Permitir acceso
│
Sí
│
▼
¿Usuario tiene rol?──── No ──→ ❌ 403 ForbiddenException
│
Sí
│
▼
✅ Permitir acceso
Detalle clave sobre getAllAndOverride: se usa en lugar de get porque resuelve metadata tanto del handler (método) como de la clase (controlador), priorizando el handler. Esto permite poner @Public() o @Roles() a nivel de controlador y sobreescribirlo en métodos específicos.
Criterios evaluados
Uso idiomático de SetMetadata para decoradores, Reflector.getAllAndOverride para metadata a nivel handler/clase y ExecutionContext.switchToHttp().getRequest(). El patrón de guard Injectable con DI de JwtService y Reflector es el esperado.
Uso idiomático perfecto: Reflector.getAllAndOverride() con [getHandler(), getClass()], SetMetadata() para decoradores custom, ExecutionContext.switchToHttp().getRequest(), @Injectable(), APP_GUARD provider pattern. Incluso explica por qué getAllAndOverride en lugar de get.
Sin token lanza UnauthorizedException, verifyAsync está envuelto en try/catch y ante error lanza UnauthorizedException, y si roles no coinciden lanza ForbiddenException. La distinción es correcta; solo podría considerarse que no diferencia explícitamente expirado vs inválido, pero el spec los agrupa.
Manejo de errores impecable: sin token → UnauthorizedException, token inválido/expirado capturado con try/catch alrededor de verifyAsync → UnauthorizedException, roles insuficientes → ForbiddenException. También maneja el caso donde payload.roles es undefined/null antes de verificar inclusión.
Cumple todos los requisitos del spec: AuthGuard implementa CanActivate, extrae Bearer token del header Authorization, usa JwtService.verifyAsync, adjunta payload a request.user, @Public() con IS_PUBLIC_KEY='isPublic', @Roles() con ROLES_KEY='roles', verifica roles con some/includes, usa Reflector, y lanza las excepciones correctas. No entrega módulo como se pidió. Mínimo desvío: se exportan las constantes de metadata junto con los decoradores (lo cual es correcto y útil), y se incluye ejemplo de registro como global guard que no fue solicitado pero no contradice el spec.
Implementa CanActivate, extrae Bearer del Authorization, valida con JwtService.verifyAsync, setea request.user y soporta @Public() (IS_PUBLIC_KEY) y @Roles() (ROLES_KEY) con chequeo de inclusión. Pequeñas diferencias no especificadas: tipado/lectura de roles desde payload.roles sin validar estructura y mensajes custom en excepciones.
P1.3code-generation58.4
Respuesta del modelo
Módulo de Autenticación — NestJS
Estructura de archivos
src/auth/
├── dto/
│ ├── login.dto.ts
│ └── auth-response.dto.ts
├── auth.controller.ts
└── auth.service.ts
DTOs
dto/login.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class LoginDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
}
dto/auth-response.dto.ts
export class AuthResponseDto {
access_token: string;
expiresIn: number;
}
Service
auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { UserService } from '../user/user.service';
import { AuthResponseDto } from './dto/auth-response.dto';
@Injectable()
export class AuthService {
private static readonly EXPIRES_IN_SECONDS = 604800; // 7 days
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}
async login(email: string, password: string): Promise<AuthResponseDto> {
const user = await this.userService.findByEmail(email);
if (!user) {
throw new UnauthorizedException('Invalid credentials');
}
const passwordMatches = await bcrypt.compare(password, user.passwordHash);
if (!passwordMatches) {
throw new UnauthorizedException('Invalid credentials');
}
const payload = { sub: user.id, email: user.email };
const accessToken = await this.jwtService.signAsync(payload, {
expiresIn: '7d',
});
const response = new AuthResponseDto();
response.access_token = accessToken;
response.expiresIn = AuthService.EXPIRES_IN_SECONDS;
return response;
}
}
Controller
auth.controller.ts
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';
import { AuthResponseDto } from './dto/auth-response.dto';
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@HttpCode(HttpStatus.OK)
async login(@Body() loginDto: LoginDto): Promise<AuthResponseDto> {
return this.authService.login(loginDto.email, loginDto.password);
}
}
Decisiones de diseño y notas
Por qué @HttpCode(HttpStatus.OK) es explícito
NestJS asigna 201 por defecto a los endpoints @Post(). Un login no crea un recurso, devuelve una credencial existente conceptualmente. El decorador fuerza el 200 que pide la especificación.
Por qué el Service lanza UnauthorizedException directamente
La especificación dice que el Service "no conoce HTTP excepto para lanzar excepciones de autenticación". UnauthorizedException es exactamente eso: una excepción semántica de autenticación que NestJS traduce a HTTP 401. No se filtra ningún otro concepto HTTP al Service.
Por qué usuario-no-encontrado y contraseña-incorrecta devuelven el mismo mensaje
Ambas ramas lanzan "Invalid credentials" sin distinguir cuál falló. Esto es deliberado: diferenciar los mensajes permite a un atacante enumerar emails válidos en el sistema (user enumeration attack).
Por qué signAsync en lugar de sign
sign es síncrono y bloquea el event loop durante la firma criptográfica. signAsync delega al thread pool de libuv. En un endpoint de login que puede recibir ráfagas de peticiones, la diferencia importa.
Por qué no se hardcodea el secret en signAsync
El JwtService ya está configurado con JWT_SECRET a nivel de módulo (según la especificación). Pasarlo de nuevo en signAsync crearía una segunda fuente de verdad. Si alguien cambia la configuración del módulo pero olvida este archivo, los tokens se firmarían con un secret distinto al esperado.
Por qué expiresIn es una constante estática
El valor 604800 es la traducción exacta de '7d' a segundos. Se expone como constante con nombre (EXPIRES_IN_SECONDS) para que si cambia la duración del token, un grep por la constante encuentre ambos sitios que necesitan actualizarse (la constante y el string '7d' en signAsync).
Criterios evaluados
El controller solo recibe el DTO y delega al service sin lógica de negocio, y el service concentra validación de credenciales y generación del token. El service solo usa UnauthorizedException como excepción de autenticación, sin depender de objetos HTTP.
El Controller solo recibe el DTO, extrae email y password, y delega al Service sin ninguna lógica de negocio. El Service no importa nada de HTTP excepto UnauthorizedException, que es exactamente la excepción permitida según la especificación. Separación perfecta de capas.
Implementa POST /auth/login con body {email, password}, DTOs con los validators pedidos, usa userService.findByEmail, bcrypt.compare y jwtService.signAsync con payload {sub, email} y expiresIn '7d', y responde {access_token, expiresIn: 604800}. El único desvío menor es agregar notas/decisiones extra (no afecta el código) y asumir la ruta de import de UserService.
Cumple todos los requisitos del spec: LoginDto con @IsEmail, @IsString, @MinLength(8); AuthResponseDto con access_token y expiresIn; el service llama findByEmail, bcrypt.compare, jwtService.signAsync con payload {sub: user.id, email: user.email}; expiresIn '7d' y 604800; UnauthorizedException con 'Invalid credentials'; importa bcrypt de 'bcrypt'; @HttpCode(200) para el POST. El único detalle menor es que usa signAsync en lugar de sign, lo cual es una mejora válida pero técnicamente una desviación mínima del patrón más común — aunque no contradice el spec.
Usa bcrypt.compare correctamente y devuelve el mismo mensaje 'Invalid credentials' tanto si el usuario no existe como si la contraseña falla, evitando enumeración. No hay rate limiting/lockout (fuera de spec), pero lo implementado no introduce vulnerabilidades obvias.
Usa bcrypt.compare correctamente, ambas ramas de fallo (usuario no encontrado y contraseña incorrecta) devuelven el mismo mensaje 'Invalid credentials' evitando enumeración de usuarios, y el secret no se hardcodea. Además, el modelo explica explícitamente por qué estas decisiones son importantes desde el punto de vista de seguridad.
P4.1code-generation56.5
Respuesta del modelo
Implementación
DTOs
// dto/search-products.dto.ts
import {
IsOptional,
IsString,
IsNumber,
IsEnum,
IsInt,
Min,
Max,
} from 'class-validator';
import { Type } from 'class-transformer';
export class SearchProductsDto {
@IsOptional()
@IsString()
q?: string;
@IsOptional()
@IsEnum(['active', 'inactive', 'draft'])
status?: 'active' | 'inactive' | 'draft';
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(0)
minPrice?: number;
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(0)
maxPrice?: number;
@IsOptional()
@IsString()
sortBy?: string = 'createdAt';
@IsOptional()
@IsEnum(['ASC', 'DESC'])
sortOrder?: 'ASC' | 'DESC' = 'DESC';
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
limit?: number = 10;
}
// dto/product.dto.ts
export class ProductDto {
id: string;
name: string;
description: string | null;
price: number;
stock: number;
status: string;
createdAt: Date;
}
// dto/paginated-products.dto.ts
import { ProductDto } from './product.dto';
export class PaginatedProductsDto {
data: ProductDto[];
total: number;
page: number;
limit: number;
}
Servicio
// products.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './product.entity';
import { SearchProductsDto } from './dto/search-products.dto';
import { PaginatedProductsDto } from './dto/paginated-products.dto';
import { ProductDto } from './dto/product.dto';
@Injectable()
export class ProductsService {
private static readonly ALLOWED_SORT_FIELDS = new Set([
'name',
'price',
'stock',
'createdAt',
]);
constructor(
@InjectRepository(Product)
private readonly productRepository: Repository<Product>,
) {}
async search(query: SearchProductsDto): Promise<PaginatedProductsDto> {
const page = query.page ?? 1;
const limit = query.limit ?? 10;
const sortOrder = query.sortOrder ?? 'DESC';
const sortBy = ProductsService.ALLOWED_SORT_FIELDS.has(query.sortBy)
? query.sortBy
: 'createdAt';
const qb = this.productRepository.createQueryBuilder('product');
// Soft-delete exclusion — siempre presente
qb.where('product.deletedAt IS NULL');
// Búsqueda de texto libre en name y description
if (query.q !== undefined && query.q.trim() !== '') {
qb.andWhere(
'(product.name ILIKE :q OR product.description ILIKE :q)',
{ q: `%${query.q.trim()}%` },
);
}
// Filtro por status exacto
if (query.status !== undefined) {
qb.andWhere('product.status = :status', { status: query.status });
}
// Rango de precio
if (query.minPrice !== undefined) {
qb.andWhere('product.price >= :minPrice', { minPrice: query.minPrice });
}
if (query.maxPrice !== undefined) {
qb.andWhere('product.price <= :maxPrice', { maxPrice: query.maxPrice });
}
// Sorting
qb.orderBy(`product.${sortBy}`, sortOrder);
// Paginación
const offset = (page - 1) * limit;
qb.skip(offset).take(limit);
// Ejecución: datos + count en una sola query
const [products, total] = await qb.getManyAndCount();
return {
data: products.map(this.toProductDto),
total,
page,
limit,
};
}
private toProductDto(product: Product): ProductDto {
return {
id: product.id,
name: product.name,
description: product.description,
price: product.price,
stock: product.stock,
status: product.status,
createdAt: product.createdAt,
};
}
}
Cómo funciona cada pieza
Flujo del QueryBuilder:
WHERE deletedAt IS NULL ← siempre (soft-delete)
AND (name ILIKE OR desc ILIKE) ← solo si `q` existe y no está vacío
AND status = :status ← solo si `status` existe
AND price >= :minPrice ← solo si `minPrice` existe
AND price <= :maxPrice ← solo si `maxPrice` existe
ORDER BY :sortBy :sortOrder
OFFSET :offset LIMIT :limit
Puntos clave:
getManyAndCount()ejecuta internamente dos queries (SELECT + COUNT) pero respeta todos los filtros aplicados al builder, devolviendo eltotalsin paginación y losdatacon paginación.- Sanitización del
sortBy: se valida contra unSetde campos permitidos. Cualquier valor fuera de esa lista cae a'createdAt', evitando inyección de columnas arbitrarias en elORDER BY. ILIKEcon parámetros: el%se concatena en el valor del parámetro ({ q: '%term%' }), nunca interpolado en la query string, por lo que TypeORM lo parametriza correctamente y no hay riesgo de SQL injection.- Cada
andWherese encadena condicionalmente: siquery.minPriceesundefined, esa cláusula no existe en la query final — cero filtros innecesarios.
Criterios evaluados
Usa createQueryBuilder('product'), agrega filtros solo si vienen en el DTO y aplica paginación con skip/take. Utiliza getManyAndCount() correctamente para datos + total con filtros (count sin paginación).
Usa correctamente createQueryBuilder('product'), encadena andWhere condicionalmente para cada filtro (q, status, minPrice, maxPrice), y utiliza getManyAndCount() para obtener datos y total en una sola llamada. El único detalle menor es que getManyAndCount() internamente ejecuta dos queries, no una, pero la descripción en la explicación lo aclara y el uso es el solicitado por el spec.
Cumple q con ILIKE en name/description con OR, excluye deletedAt != null y valida sortBy con fallback a createdAt. Pequeñas desviaciones: el DTO permite sortBy como string libre (sin @IsIn) y @IsEnum usado con arrays en class-validator no es la forma canónica, aunque la lógica del servicio sí cumple el spec.
El filtro q busca en name y description con OR usando ILIKE. sortBy valida contra un Set de campos permitidos con fallback a createdAt. Siempre filtra deletedAt IS NULL. Los defaults de page, limit, sortBy y sortOrder están correctos. Un detalle menor: la validación de q incluye un trim() adicional no solicitado pero razonable, y el check de string vacía es un buen extra defensivo.
sortBy se valida contra un Set de campos permitidos antes de interpolarse en orderBy, evitando inyección; el resto de filtros usan parámetros. Solo queda un riesgo teórico si query.sortBy no es string (p.ej. objeto) pero el Set.has fallará y caerá al fallback seguro.
Implementa una whitelist con Set de campos permitidos ('name', 'price', 'stock', 'createdAt') y hace fallback a 'createdAt' si el valor no está en la lista. El valor del usuario nunca se interpola directamente en la query SQL. La única mínima observación es que el template literal `product.${sortBy}` técnicamente interpola, pero dado que sortBy ya fue validado contra la whitelist, es seguro.
P5.1refactoring70.3
Respuesta del modelo
Refactoring: Arquitectura Controller → Service → Repository
Análisis del código original
El código viola la arquitectura en tres puntos críticos:
- Controller contiene lógica de negocio (hashing, validación de duplicados, queries con relaciones)
- Retorna entidades directamente (expone
passwordHash) - No usa DTOs con validación (recibe
bodysin tipo ni decoradores)
Código refactorizado
Entidades (sin cambios, solo separadas en archivos)
// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from 'typeorm';
import { Order } from './order.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid') id: string;
@Column({ unique: true }) email: string;
@Column() name: string;
@Column() passwordHash: string;
@CreateDateColumn() createdAt: Date;
@OneToMany(() => Order, o => o.user) orders: Order[];
}
// order.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, ManyToOne } from 'typeorm';
import { User } from './user.entity';
@Entity()
export class Order {
@PrimaryGeneratedColumn('uuid') id: string;
@Column() total: number;
@Column({ default: 'pending' }) status: string;
@CreateDateColumn() createdAt: Date;
@ManyToOne(() => User, u => u.orders) user: User;
}
DTOs
// dto/create-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(1)
name: string;
@IsString()
@MinLength(6)
password: string;
}
// dto/user.dto.ts
export class UserDto {
id: string;
email: string;
name: string;
createdAt: Date;
}
// dto/user-with-orders.dto.ts
export class UserWithOrdersDto {
id: string;
email: string;
name: string;
createdAt: Date;
orderCount: number;
}
Repository
// user.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserRepository {
constructor(
@InjectRepository(User) private readonly repo: Repository<User>,
) {}
async findById(id: string): Promise<User | null> {
return this.repo.findOne({
where: { id },
relations: ['orders'],
});
}
async findByEmail(email: string): Promise<User | null> {
return this.repo.findOne({ where: { email } });
}
async create(data: { email: string; name: string; passwordHash: string }): Promise<User> {
const user = this.repo.create(data);
return this.repo.save(user);
}
}
Decisión clave: findById siempre carga orders porque el único caso de uso actual (findOneWithOrders) lo necesita. Si en el futuro existiera un findOne sin órdenes, se agregaría un método separado. No se sobre-diseña.
Service
// user.service.ts
import { Injectable, ConflictException } from '@nestjs/common';
import * as bcrypt from 'bcrypt';
import { UserRepository } from './user.repository';
import { CreateUserDto } from './dto/create-user.dto';
import { UserDto } from './dto/user.dto';
import { UserWithOrdersDto } from './dto/user-with-orders.dto';
import { User } from './user.entity';
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
async create(dto: CreateUserDto): Promise<UserDto> {
const existing = await this.userRepository.findByEmail(dto.email);
if (existing) {
throw new ConflictException('Email already registered');
}
const passwordHash = await bcrypt.hash(dto.password, 10);
const user = await this.userRepository.create({
email: dto.email,
name: dto.name,
passwordHash,
});
return this.toUserDto(user);
}
async findOneWithOrders(id: string): Promise<UserWithOrdersDto | null> {
const user = await this.userRepository.findById(id);
if (!user) {
return null;
}
return this.toUserWithOrdersDto(user);
}
// --- Mappers privados ---
private toUserDto(user: User): UserDto {
const dto = new UserDto();
dto.id = user.id;
dto.email = user.email;
dto.name = user.name;
dto.createdAt = user.createdAt;
return dto;
}
private toUserWithOrdersDto(user: User): UserWithOrdersDto {
const dto = new UserWithOrdersDto();
dto.id = user.id;
dto.email = user.email;
dto.name = user.name;
dto.createdAt = user.createdAt;
dto.orderCount = user.orders?.length ?? 0;
return dto;
}
}
Por qué ConflictException vive en el Service: "Email ya registrado" es una regla de negocio. No es un problema de formato de request (eso sería controller) ni de persistencia (eso sería repository). El service decide qué constituye un conflicto de dominio.
Por qué retorna null en vez de lanzar NotFoundException: "No encontrado" no es un error de negocio, es una decisión de presentación HTTP. El service dice "no existe", el controller decide qué status code corresponde.
Controller
// user.controller.ts
import {
Controller,
Post,
Get,
Param,
Body,
NotFoundException,
HttpCode,
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UserDto } from './dto/user.dto';
import { UserWithOrdersDto } from './dto/user-with-orders.dto';
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Post()
@HttpCode(201)
async create(@Body() dto: CreateUserDto): Promise<UserDto> {
return this.userService.create(dto);
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<UserWithOrdersDto> {
const user = await this.userService.findOneWithOrders(id);
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
}
Module
// user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { Order } from './order.entity';
import { UserRepository } from './user.repository';
import { UserService } from './user.service';
import { UserController } from './user.controller';
@Module({
imports: [TypeOrmModule.forFeature([User, Order])],
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService],
})
export class UserModule {}
Verificación de comportamiento preservado
| Aspecto | Antes | Después |
|---|---|---|
| POST /users con email nuevo | 201 + entidad completa (con passwordHash) | 201 + UserDto (sin passwordHash) ✓ más seguro |
| POST /users con email duplicado | 409 "Email already registered" | 409 "Email already registered" ✓ idéntico |
| GET /users/:id existente | 200 + {...user, orderCount} | 200 + UserWithOrdersDto ✓ misma forma |
| GET /users/:id inexistente | 404 "User not found" | 404 "User not found" ✓ idéntico |
| Password hashing | bcrypt, 10 rounds | bcrypt, 10 rounds ✓ idéntico |
Nota: El único cambio observable es que
POST /usersya no exponepasswordHashen la respuesta. Esto es una corrección de seguridad, no un cambio de comportamiento funcional.
Criterios evaluados
Están todos los DTOs pedidos (CreateUserDto, UserDto, UserWithOrdersDto) y UserDto no incluye `passwordHash`; UserWithOrdersDto incluye `orderCount`. Las validaciones están, aunque las “restricciones implícitas” del original no estaban definidas y se asumieron (@MinLength(1) para name y @MinLength(6) para password).
Los tres DTOs requeridos están implementados correctamente: CreateUserDto con @IsEmail, @IsString y @MinLength de class-validator; UserDto con id, email, name, createdAt sin passwordHash; UserWithOrdersDto con orderCount. La única observación menor es que el MinLength(6) para password es una decisión razonable pero arbitraria (no estaba explícita en el código original), y los DTOs de respuesta no usan decoradores de class-validator (lo cual es correcto ya que son de salida).
Cambia el comportamiento observable en POST /users: antes devolvía la entidad completa (incluyendo passwordHash) y ahora devuelve UserDto sin passwordHash, lo cual contradice el requisito explícito de “sin cambiar comportamiento observable”. El resto (201, 409, GET con orderCount, 404) sí se mantiene.
El comportamiento se preserva correctamente: POST /users → 201, GET /users/:id → 200 con orderCount, email duplicado → 409 ConflictException, id no encontrado → 404 NotFoundException. El Service retorna null y el Controller lanza NotFoundException como se requiere. La única diferencia es que POST ya no expone passwordHash, lo cual el autor correctamente identifica como corrección de seguridad y es parte explícita de los requerimientos (UserDto sin passwordHash).
El Controller queda sin lógica de negocio y delega correctamente, el Service concentra hashing/conflicto y devuelve DTOs, y el Repository encapsula el acceso a TypeORM. Como detalle discutible, el Repository incluye `relations: ['orders']` dentro de `findById`, mezclando una decisión de caso de uso en un método genérico.
Las capas están perfectamente separadas: el Controller solo valida DTO y delega al Service (convirtiendo null en NotFoundException), el Service contiene toda la lógica de negocio (hashing, verificación de duplicados, mapping a DTOs) y retorna DTOs nunca entidades, el Repository encapsula la persistencia con métodos simples. La única observación menor es que findById siempre carga relations ['orders'], lo cual acopla ligeramente el repository a un caso de uso específico, aunque el autor lo reconoce explícitamente y justifica la decisión.
Prompt Eng.
3 pruebas
PE1.3optimization82.9
Respuesta del modelo
Prompt con Chain-of-Thought para Evaluación de Crédito Empresarial
Diseño del Prompt
# SISTEMA DE EVALUACIÓN DE CRÉDITO EMPRESARIAL
Sos un analista de riesgo crediticio automatizado. Tu trabajo es evaluar la elegibilidad
de una empresa para crédito empresarial aplicando reglas estrictas en orden secuencial.
## DATOS DE ENTRADA
Se te proporcionará un JSON con la siguiente estructura:
```json
{
"nombre_empresa": "string",
"sector": "string",
"antiguedad_anios": number,
"ingresos_anuales_usd": number,
"cantidad_empleados": number,
"moras_ultimos_12_meses": number,
"moras_historicas_totales": number,
"deuda_existente_usd": number | null,
"tiene_deuda_existente": boolean
}
INSTRUCCIONES DE RAZONAMIENTO (OBLIGATORIO SEGUIR ESTE ORDEN)
Antes de emitir cualquier veredicto, DEBÉS razonar paso a paso en voz alta, evaluando CADA regla por separado. No saltes ningún paso. No combines pasos.
PASO 1 — Clasificación de la empresa
- Determiná si es PyME (< 50 empleados) o Gran Empresa (>= 50 empleados).
- Determiná si pertenece al sector construcción (comparar sector en minúsculas contra: "construcción", "construccion", "construction", "obra civil").
- Escribí explícitamente: "Clasificación: [PyME/Gran Empresa], Sector construcción: [SÍ/NO]"
PASO 2 — Regla de Antigüedad (mínimo 2 años)
- Compará antiguedad_anios >= 2.
- Si NO cumple: marcá como DESCALIFICANTE y anotá el motivo.
- Escribí: "Antigüedad: [X] años → [CUMPLE/NO CUMPLE]"
PASO 3 — Regla de Ingresos (> $500,000 USD)
- Compará ingresos_anuales_usd > 500000 (estrictamente mayor, no igual).
- Si NO cumple: marcá como DESCALIFICANTE y anotá el motivo.
- Escribí: "Ingresos: $[X] → [CUMPLE/NO CUMPLE]"
PASO 4 — Regla de Mora Reciente (0 moras en últimos 12 meses)
- Compará moras_ultimos_12_meses == 0.
- EXCEPCIÓN PyME: Si es PyME Y moras_ultimos_12_meses == 1 Y moras_historicas_totales <= 1, NO es descalificante, pero marcá "EXCEPCIÓN PyME APLICADA → tasa se incrementará".
- Si es PyME Y moras_ultimos_12_meses > 1: DESCALIFICANTE.
- Si NO es PyME Y moras_ultimos_12_meses > 0: DESCALIFICANTE.
- Escribí: "Moras recientes: [X] → [CUMPLE/NO CUMPLE/EXCEPCIÓN PyME]"
PASO 5 — Regla de Ratio Deuda/Ingreso (solo si tiene deuda existente)
- Si tiene_deuda_existente == false o deuda_existente_usd es null o 0: escribí "Sin deuda existente → REGLA NO APLICA" y pasá al siguiente paso.
- Si tiene deuda: calculá ratio = deuda_existente_usd / ingresos_anuales_usd.
- Mostrá el cálculo explícitamente: "Ratio = $[deuda] / $[ingresos] = [resultado]"
- Si ratio >= 0.4: DESCALIFICANTE.
- Escribí: "Ratio deuda/ingreso: [X] → [CUMPLE/NO CUMPLE/NO APLICA]"
PASO 6 — Requisito de Garantía (sector construcción)
- Si es sector construcción (determinado en Paso 1): requiere_garantia = true INDEPENDIENTEMENTE de todos los demás resultados.
- Si NO es sector construcción: requiere_garantia = false.
- Escribí: "Garantía adicional requerida: [SÍ/NO]"
PASO 7 — Consolidación y Veredicto
Revisá todos los pasos anteriores y consolidá:
7a. ELEGIBILIDAD:
- Si hay AL MENOS UN resultado DESCALIFICANTE → elegible = false
- Si todos los criterios se cumplen (incluyendo excepciones PyME) → elegible = true
7b. TASA DE RIESGO (aplicar la primera regla que coincida, en este orden):
- Si elegible == false → tasa_riesgo = "rechazado"
- Si se aplicó EXCEPCIÓN PyME → tasa_riesgo = "alta"
- Si ratio deuda/ingreso existe y está entre 0.3 y 0.4 (exclusive) → tasa_riesgo = "media"
- Si ratio deuda/ingreso existe y está entre 0.2 y 0.3 (inclusive) → tasa_riesgo = "media"
- Si antiguedad_anios < 5 → tasa_riesgo = "media"
- En cualquier otro caso → tasa_riesgo = "baja"
7c. MOTIVOS:
- Listá TODOS los motivos relevantes (tanto positivos como negativos).
- Cada motivo debe referenciar la regla específica que lo origina.
FORMATO DE SALIDA
Después del razonamiento paso a paso, generá EXACTAMENTE este bloque JSON final
delimitado por json y :
{
"elegible": boolean,
"motivos": ["string", "string", ...],
"tasa_riesgo": "baja" | "media" | "alta" | "rechazado",
"requiere_garantia": boolean,
"detalle_evaluacion": {
"antiguedad_cumple": boolean,
"ingresos_cumple": boolean,
"mora_cumple": boolean,
"excepcion_pyme_aplicada": boolean,
"ratio_deuda_ingreso": number | null,
"ratio_cumple": boolean | null
}
}
REGLAS CRÍTICAS (NO VIOLAR BAJO NINGUNA CIRCUNSTANCIA)
- NUNCA declares elegible a una empresa con algún criterio DESCALIFICANTE.
- NUNCA ignores la excepción PyME si aplica.
- SIEMPRE evaluá la garantía de construcción, incluso si la empresa es rechazada.
- Si hay datos faltantes o inconsistentes en el JSON, marcá elegible = false con motivo "Datos insuficientes o inconsistentes para evaluación".
EMPRESA A EVALUAR:
{{EMPRESA_JSON}}
---
## Ejemplo de Ejecución Completa
### Input
```json
{
"nombre_empresa": "Constructora del Litoral SRL",
"sector": "construcción",
"antiguedad_anios": 4,
"ingresos_anuales_usd": 820000,
"cantidad_empleados": 35,
"moras_ultimos_12_meses": 1,
"moras_historicas_totales": 1,
"deuda_existente_usd": 200000,
"tiene_deuda_existente": true
}
Razonamiento esperado del LLM
=== EVALUACIÓN DE CRÉDITO: Constructora del Litoral SRL ===
PASO 1 — Clasificación:
- Cantidad de empleados: 35 → 35 < 50 → ES PyME ✓
- Sector: "construcción" → coincide con sector construcción → SÍ
- Clasificación: PyME, Sector construcción: SÍ
PASO 2 — Antigüedad:
- Antigüedad: 4 años → 4 >= 2 → CUMPLE ✓
PASO 3 — Ingresos:
- Ingresos: $820,000 → $820,000 > $500,000 → CUMPLE ✓
PASO 4 — Mora Reciente:
- Moras últimos 12 meses: 1 → NO es 0.
- ¿Es PyME? SÍ.
- ¿moras_ultimos_12_meses == 1? SÍ.
- ¿moras_historicas_totales <= 1? 1 <= 1 → SÍ.
- EXCEPCIÓN PyME APLICADA → tasa se incrementará.
- Moras recientes: 1 → EXCEPCIÓN PyME ✓
PASO 5 — Ratio Deuda/Ingreso:
- tiene_deuda_existente: true, deuda: $200,000
- Ratio = $200,000 / $820,000 = 0.2439
- 0.2439 < 0.4 → CUMPLE ✓
- Ratio deuda/ingreso: 0.2439 → CUMPLE
PASO 6 — Garantía:
- Sector construcción: SÍ → requiere_garantia = true
- Garantía adicional requerida: SÍ
PASO 7 — Consolidación:
7a. Criterios descalificantes: NINGUNO → elegible = true
7b. Tasa de riesgo:
- ¿elegible == false? NO
- ¿Se aplicó excepción PyME? SÍ → tasa_riesgo = "alta"
7c. Motivos:
- Antigüedad suficiente (4 años)
- Ingresos cumplen requisito mínimo
- Excepción PyME aplicada: 1 mora reciente tolerada con tasa incrementada
- Ratio deuda/ingreso saludable (0.24)
- Requiere garantía adicional por ser sector construcción
Output
{
"elegible": true,
"motivos": [
"Antigüedad suficiente: 4 años (mínimo requerido: 2)",
"Ingresos anuales cumplen requisito: $820,000 > $500,000",
"Excepción PyME aplicada: 1 mora en últimos 12 meses tolerada (tasa incrementada)",
"Ratio deuda/ingreso dentro de límite: 0.2439 < 0.4",
"Garantía adicional requerida por pertenecer al sector construcción"
],
"tasa_riesgo": "alta",
"requiere_garantia": true,
"detalle_evaluacion": {
"antiguedad_cumple": true,
"ingresos_cumple": true,
"mora_cumple": true,
"excepcion_pyme_aplicada": true,
"ratio_deuda_ingreso": 0.2439,
"ratio_cumple": true
}
}
¿Por qué este diseño funciona?
PROBLEMA SOLUCIÓN EN EL PROMPT
─────────────────────────────────────────────────────────────────
Reglas condicionales anidadas → Cada regla es un PASO aislado
(PyME + mora + histórico) con sub-condiciones explícitas
El LLM "olvida" excepciones → La excepción PyME está embebida
DENTRO del paso de mora, no separada
Saltos lógicos del modelo → Formato obligatorio "Escribí:"
fuerza verbalización de cada paso
Sector construcción ignorado → Paso 6 es INDEPENDIENTE y se evalúa
si la empresa es rechazada "incluso si la empresa es rechazada"
Ambigüedad en tasa de riesgo → Reglas ordenadas por PRIORIDAD:
"aplicar la primera que coincida"
Datos faltantes o basura → Regla crítica explícita de fallback
→ elegible = false automático
Principios de Prompt Engineering aplicados
| Principio | Aplicación |
|---|---|
| Chain-of-Thought | 7 pasos secuenciales obligatorios antes del veredicto |
| Formato forzado | Cada paso exige una frase de cierre con formato fijo |
| Prioridad explícita | Las reglas de tasa se aplican en orden declarado |
| Aislamiento de reglas | Cada condicional vive en su propio paso, evitando cortocircuitos |
| Guardrails negativos | Sección "REGLAS CRÍTICAS" con prohibiciones explícitas |
| Trazabilidad | detalle_evaluacion permite auditar cada decisión individual |
Criterios evaluados
Define un flujo secuencial por pasos (1–7) y fuerza la verificación explícita de cada regla antes del veredicto. Sin embargo, exige “razonar en voz alta”, lo cual es riesgoso/innecesario en producción y no es la forma más segura de CoT.
Implementación excelente de CoT con 7 pasos secuenciales obligatorios, cada uno con formato de cierre forzado ('Escribí:') que obliga al LLM a verbalizar su razonamiento antes de consolidar. La separación entre razonamiento y veredicto final es clara y bien estructurada.
Cubre antigüedad, ingresos, mora reciente, ratio deuda/ingreso, excepción PyME y garantía para construcción. Pero la excepción PyME está mal mapeada: la regla original permite 1 mora histórica (no necesariamente en los últimos 12 meses) y aquí se permite 1 mora reciente bajo condiciones adicionales, cambiando la lógica.
Todas las 6 reglas están cubiertas explícitamente, incluyendo las condicionales de PyME y construcción. La excepción PyME está bien integrada dentro del paso de mora. Pequeña observación: la regla 5 original dice 'puede tener 1 mora histórica' pero el prompt la interpreta como 1 mora reciente con moras históricas <=1, lo cual es una interpretación razonable pero ligeramente distinta de la regla original que habla de 'mora histórica'.
Aísla reglas y prioriza condiciones, lo cual ayuda, pero introduce lógica incorrecta en la excepción PyME y añade un esquema propio de tasa_riesgo no especificado (umbrales 0.2/0.3/antigüedad<5), creando riesgo de decisiones inconsistentes con el policy real.
El diseño aísla cada regla en su propio paso, la prioridad de tasa_riesgo está explícitamente ordenada, y las 'REGLAS CRÍTICAS' actúan como guardrails efectivos. La inclusión de manejo de datos faltantes y la evaluación de garantía independiente del resultado de elegibilidad son buenas prácticas. La tabla resumen de problemas/soluciones demuestra pensamiento deliberado sobre edge cases. Pierde puntos porque la excepción PyME podría ser más clara respecto a la interacción entre moras recientes e históricas.
Fuerza un bloque JSON final con los campos requeridos y tipos explícitos, lo que suele mejorar consistencia. Agrega un campo extra (detalle_evaluacion) que no fue pedido, aunque no rompe el requisito mínimo.
El formato JSON de salida está definido con precisión, incluyendo tipos de datos y un campo adicional 'detalle_evaluacion' que mejora la trazabilidad. El ejemplo completo de ejecución refuerza el formato esperado. El único riesgo menor es que el campo detalle_evaluacion no estaba en los requisitos originales, pero no contradice nada.
Es muy largo, incluye ejemplo completo, tablas y explicación meta que no son necesarias para ejecutar la tarea. También introduce reglas adicionales (heurísticas de tasa) no solicitadas, aumentando superficie de error y costo de tokens.
El prompt es bastante extenso con la tabla explicativa final, la sección de principios y el ejemplo completo, lo cual incrementa el consumo de tokens significativamente. Si bien el ejemplo es útil para anclar el comportamiento, la sección '¿Por qué este diseño funciona?' y la tabla de principios son meta-explicaciones para el lector humano, no para el LLM, lo que añade verbosidad innecesaria al prompt operativo.
PE1.2optimization90.0
Respuesta del modelo
Prompt de Sistema para Clasificación Automática de Tickets de Soporte
Eres un sistema experto de clasificación automática de tickets de soporte al cliente. Tu única función es analizar el mensaje del cliente y devolver un JSON estructurado. No conversas, no saludas, no explicas. Solo devuelves el JSON.
═══════════════════════════════════════════
INSTRUCCIONES ESTRICTAS
═══════════════════════════════════════════
1. ANALIZA el mensaje del cliente proporcionado en el input.
2. DETERMINA los valores para cada campo según las reglas definidas abajo.
3. DEVUELVE ÚNICAMENTE un objeto JSON válido, sin markdown, sin bloques de código, sin texto adicional antes o después.
═══════════════════════════════════════════
FORMATO DE SALIDA (obligatorio)
═══════════════════════════════════════════
{
"categoria": "<string>",
"prioridad": "<string>",
"sentimiento": "<string>",
"requiere_humano": <boolean>,
"resumen_corto": "<string, máximo 120 caracteres>"
}
═══════════════════════════════════════════
REGLAS DE CLASIFICACIÓN POR CAMPO
═══════════════════════════════════════════
── CATEGORIA ──
Valores permitidos (exactamente uno):
• "facturacion" → Cobros, cargos, reembolsos, facturas, métodos de pago, doble cobro, descuentos no aplicados.
• "envio" → Estado de envío, paquete perdido, demora en entrega, dirección incorrecta, seguimiento de pedido.
• "producto_defectuoso" → Producto roto, no funciona, llegó dañado, falla técnica, diferente al anunciado.
• "consulta_general" → Preguntas sobre productos, políticas, horarios, disponibilidad, cómo usar algo.
• "cancelacion" → Solicitud de cancelar pedido, suscripción, cuenta o servicio.
• "fraude" → Compra no reconocida, acceso no autorizado a cuenta, suplantación de identidad, phishing reportado.
Regla de ambigüedad: Si el mensaje toca múltiples categorías, elige la que represente la INTENCIÓN PRINCIPAL del cliente (lo que quiere que se resuelva). Si no hay suficiente información para determinar ninguna categoría, usa "consulta_general".
── PRIORIDAD ──
Valores permitidos (exactamente uno):
• "critica" → Fraude confirmado o sospechado, cobros duplicados de monto alto (>$500 o equivalente), acceso no autorizado a cuenta, situaciones con impacto legal o financiero inmediato.
• "alta" → Producto defectuoso con riesgo de seguridad, pedido perdido ya fuera de plazo, cliente explícitamente furioso o amenazando con acción legal, cobros incorrectos de cualquier monto.
• "media" → Demoras en envío dentro de plazo, productos defectuosos sin riesgo de seguridad, solicitudes de cancelación, reembolsos estándar.
• "baja" → Consultas generales, preguntas informativas, feedback positivo, solicitudes sin urgencia explícita.
Regla de escalación: Ante la duda entre dos niveles de prioridad, elige el MÁS ALTO.
── SENTIMIENTO ──
Valores permitidos (exactamente uno):
• "muy_negativo" → Insultos, amenazas, mayúsculas sostenidas, frustración extrema, múltiples signos de exclamación con enojo.
• "negativo" → Queja clara, insatisfacción, decepción, tono demandante pero sin agresividad.
• "neutro" → Tono factual, sin carga emocional evidente, solicitudes directas sin valoración.
• "positivo" → Agradecimiento, satisfacción, tono amable, feedback constructivo.
• "mixto" → El mensaje contiene señales emocionales contradictorias (ej: elogio + queja).
── REQUIERE_HUMANO ──
Devuelve true si se cumple AL MENOS UNA de estas condiciones:
• La categoría es "fraude".
• La prioridad es "critica".
• El cliente solicita explícitamente hablar con una persona/agente/supervisor/manager.
• El mensaje contiene amenazas legales (demanda, abogado, denuncia, lawsuit).
• El sentimiento es "muy_negativo".
• El mensaje es tan ambiguo o incompleto que no se puede determinar qué necesita el cliente (menos de 5 palabras con significado útil O completamente incoherente).
• El cliente menciona situaciones de vulnerabilidad (salud, emergencia, discapacidad).
En todos los demás casos, devuelve false.
── RESUMEN_CORTO ──
• Máximo 120 caracteres.
• En español, independientemente del idioma del input.
• Describe el problema o solicitud del cliente en tercera persona.
• Si el mensaje es ambiguo: "Mensaje ambiguo: [lo poco que se pueda inferir]".
• Si el mensaje es incoherente/vacío: "Mensaje no clasificable, requiere revisión manual".
═══════════════════════════════════════════
MANEJO DE IDIOMAS
═══════════════════════════════════════════
• Acepta mensajes en español, inglés, spanglish o mezclas.
• El campo "resumen_corto" SIEMPRE se escribe en español.
• La clasificación se realiza sobre el SIGNIFICADO del mensaje, no sobre el idioma.
═══════════════════════════════════════════
MANEJO DE CASOS EDGE
═══════════════════════════════════════════
• Mensaje vacío o solo emojis/símbolos → categoria: "consulta_general", prioridad: "baja", requiere_humano: true.
• Mensaje con múltiples problemas → clasifica por la intención principal; menciona los problemas secundarios en el resumen si caben en 120 caracteres.
• Spam o contenido no relacionado con soporte → categoria: "consulta_general", prioridad: "baja", requiere_humano: true, resumen: "Mensaje no relacionado con soporte, requiere revisión".
• Mensajes sarcásticos → interpreta la intención real detrás del sarcasmo para determinar sentimiento y categoría.
═══════════════════════════════════════════
EJEMPLOS (few-shot)
═══════════════════════════════════════════
--- EJEMPLO 1: Fraude con spanglish y alta carga emocional ---
INPUT:
"QUE ONDA?? Acabo de ver un charge en mi credit card por $1,200 que yo NUNCA hice!!! Alguien hackeó mi account o qué?? I need this fixed RIGHT NOW o voy a llamar a mi abogado. This is unacceptable!!!"
OUTPUT:
{"categoria":"fraude","prioridad":"critica","sentimiento":"muy_negativo","requiere_humano":true,"resumen_corto":"Cliente reporta cargo no reconocido de $1,200, sospecha hackeo y amenaza con acción legal"}
--- EJEMPLO 2: Mensaje ambiguo e incompleto ---
INPUT:
"no me llego"
OUTPUT:
{"categoria":"envio","prioridad":"media","sentimiento":"negativo","requiere_humano":true,"resumen_corto":"Mensaje ambiguo: posible problema de entrega, sin detalles de pedido ni producto"}
--- EJEMPLO 3: Múltiples problemas, tono mixto ---
INPUT:
"Hola, la verdad el servicio de ustedes siempre me ha gustado mucho, pero esta vez estoy decepcionada. Me llegó la licuadora con el vaso roto y aparte me cobraron dos veces en mi tarjeta. Quisiera que me resolvieran lo del cobro doble primero y luego ver qué hacemos con la licuadora. Gracias."
OUTPUT:
{"categoria":"facturacion","prioridad":"alta","sentimiento":"mixto","requiere_humano":false,"resumen_corto":"Cobro duplicado en tarjeta (prioridad principal) y licuadora recibida con vaso roto"}
--- EJEMPLO 4: Consulta simple en inglés ---
INPUT:
"Hi, do you guys ship to Canada? And how long would it take approximately?"
OUTPUT:
{"categoria":"consulta_general","prioridad":"baja","sentimiento":"neutro","requiere_humano":false,"resumen_corto":"Consulta sobre disponibilidad de envío a Canadá y tiempos de entrega"}
--- EJEMPLO 5: Cancelación con sarcasmo ---
INPUT:
"Wow, qué increíble servicio 👏👏👏 Tres meses pagando una suscripción que nunca pude usar porque la app NUNCA funciona. Cancélenme todo ya, y ya ni me molesto en pedir reembolso porque seguro también van a fallar en eso."
OUTPUT:
{"categoria":"cancelacion","prioridad":"alta","sentimiento":"muy_negativo","requiere_humano":true,"resumen_corto":"Solicita cancelación de suscripción por app no funcional durante 3 meses, tono sarcástico agresivo"}
═══════════════════════════════════════════
RECORDATORIOS FINALES
═══════════════════════════════════════════
• SOLO devuelves el JSON. Nada más.
• Todos los valores de string en el JSON van en minúsculas sin tildes (excepto resumen_corto que usa español natural).
• Si no estás seguro, escala: prioridad más alta + requiere_humano: true.
• Nunca inventes información que no esté en el mensaje.
• El JSON debe ser parseable por cualquier parser estándar (sin trailing commas, sin comentarios).
Notas de implementación
| Parámetro | Valor recomendado | Justificación |
|---|---|---|
| temperature | 0 | Tarea determinista de clasificación; necesitamos consistencia, no creatividad |
| top_p | 1 | Valor por defecto, sin restricción adicional al usar temp 0 |
| max_tokens | 256 | El JSON de salida nunca debería exceder ~200 tokens |
| response_format | {"type": "json_object"} | Si la API lo soporta (OpenAI, etc.), fuerza salida JSON válida |
Ejemplo de llamada a la API (OpenAI)
import openai
import json
def clasificar_ticket(mensaje_cliente: str) -> dict:
response = openai.chat.completions.create(
model="gpt-4o",
temperature=0,
max_tokens=256,
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": SYSTEM_PROMPT # El prompt completo de arriba
},
{
"role": "user",
"content": mensaje_cliente
}
]
)
return json.loads(response.choices[0].message.content)
Decisiones de diseño clave
-
Cinco ejemplos en lugar de tres: los requisitos pedían mínimo 3 ejemplos cubriendo casos edge; incluí 5 para cubrir spanglish+fraude, mensaje incompleto, problemas múltiples, consulta simple en inglés, y sarcasmo, que son los escenarios que más errores generan en producción.
-
Regla "ante la duda, escala": un falso positivo en prioridad es preferible a un falso negativo en soporte al cliente. Un ticket marcado como
altaque resulta sermediase resuelve igual; uno marcado comobajaque eracriticagenera pérdida de clientes. -
resumen_cortosiempre en español: unifica la experiencia del equipo de soporte independientemente del idioma del cliente, facilitando dashboards y búsquedas internas. -
Sin tildes en campos de clasificación:
"facturacion"en lugar de"facturación"evita problemas de encoding en sistemas downstream y simplifica comparaciones programáticas.
Criterios evaluados
Incluye 5 ejemplos variados y bien formateados, cubriendo fraude spanglish, mensaje incompleto, multi-issue, inglés y sarcasmo. Hay un detalle discutible: en el ejemplo 2 marca requiere_humano=true pese a que su propia regla de “menos de 5 palabras” lo justificaría, pero la categoría/priority podría ser debatible (envío vs consulta_general) sin más contexto.
Incluye 5 ejemplos (superando el mínimo de 3) que cubren excelentemente casos edge: spanglish con fraude y amenaza legal, mensaje ambiguo/incompleto, múltiples problemas con sentimiento mixto, consulta simple en inglés, y sarcasmo con cancelación. Los outputs están perfectamente formateados como JSON válido y las clasificaciones son coherentes con las reglas definidas.
Define un esquema JSON explícito con los 5 campos requeridos, valores permitidos y la instrucción de devolver “solo JSON” sin texto extra. No puede “garantizar” al 100% (ningún prompt lo hace), pero está muy bien acotado y agrega reglas de normalización.
El prompt define explícitamente el esquema JSON con todos los campos requeridos, tipos de datos, valores permitidos para cada campo y restricciones (ej: máximo 120 caracteres en resumen_corto). Además recomienda usar response_format JSON en la API. Pequeña deducción porque 'categoria' aparece sin tilde en los valores pero la tarea original usa 'categoría' con tilde — aunque el prompt justifica esta decisión explícitamente.
Incluye reglas para intención principal, fallback a consulta_general, y pautas para mensajes vacíos/spam/sarcásticos; también define cómo resumir cuando hay ambigüedad. Hay cierta contradicción interna: para ambigüedad dice usar consulta_general si no hay info, pero el ejemplo “no me llego” (muy corto) se clasifica como envío y además fuerza requiere_humano, lo que puede confundir la política real.
Manejo explícito y detallado: regla de ambigüedad para categorías múltiples, regla de escalación ante duda en prioridad, instrucciones para mensajes vacíos/emojis/spam/incoherentes, mensajes con múltiples problemas, y sarcasmo. El ejemplo 2 demuestra concretamente el manejo de mensajes incompletos. Muy completo.
El prompt dentro del bloque de código está listo para copiar y pegar como system prompt. Incluye además notas de implementación con parámetros recomendados y código Python de ejemplo. Pequeña deducción porque los caracteres decorativos Unicode (═══) podrían consumir tokens innecesarios y las notas de implementación fuera del bloque de código, aunque útiles, podrían confundir sobre qué es exactamente el system prompt.
El bloque principal sirve como system prompt, pero la respuesta completa trae texto adicional (título, notas de implementación, tabla, código) que viola el requisito de “pegable directamente” sin recorte. Requiere que el implementador extraiga solo el bloque del prompt para usarlo en producción.
Aborda explícitamente español, inglés y spanglish en una sección dedicada. Los ejemplos cubren los tres casos (ejemplo 1: spanglish, ejemplo 4: inglés puro, otros en español). La regla de que resumen_corto siempre va en español es una decisión práctica bien justificada. Clasifica por significado, no por idioma.
Declara explícitamente español, inglés y spanglish, y recalca clasificar por significado; además fija resumen_corto siempre en español. Los ejemplos cubren mezcla y inglés puro, reforzando el comportamiento.
PE1.1optimization90.5
Respuesta del modelo
Prompt Production-Ready para Respuestas de Soporte E-commerce
Diagnóstico del prompt original
"Ayudame con el email" falla porque:
- No define rol → el modelo no sabe desde qué posición responder
- No da contexto → no sabe qué tipo de email ni sobre qué
- No especifica formato → cada respuesta tiene estructura distinta
- No establece tono → varía entre formal frío y casual inapropiado
- No tiene constraints → genera placeholders, inventa datos, alucina políticas
Prompt reescrito
## Rol
Sos un agente de soporte al cliente de {{nombre_empresa}}, una empresa
de ecommerce. Tu trabajo es redactar la respuesta final al email del
cliente, lista para enviar sin modificaciones.
## Contexto del caso
- **Tipo de reclamo**: {{tipo_reclamo}} (envío / devolución / pago)
- **Nombre del cliente**: {{nombre_cliente}}
- **Número de pedido**: {{numero_pedido}}
- **Resumen del problema**: {{resumen_problema}}
- **Resolución a comunicar**: {{resolucion}}
## Email original del cliente
{{email_cliente}}
## Instrucciones
1. Redactá UNA respuesta de email dirigida al cliente.
2. Comunicá exactamente la resolución indicada arriba. No inventes
políticas, plazos ni compensaciones que no estén en el campo
"Resolución a comunicar".
3. Si algún dato del contexto está vacío o dice "N/A", no lo
menciones ni lo reemplaces con texto genérico.
## Tono
- Profesional y empático: reconocé la frustración sin ser
condescendiente.
- Usá "vos" (tuteo rioplatense) / o "usted" según indique
{{registro_formal}}. Por defecto, usá "usted".
- Evitá frases vacías como "entendemos tu frustración" sin ofrecer
algo concreto a continuación.
## Formato obligatorio
Asunto: [asunto breve y descriptivo del caso]
[Saludo con nombre del cliente]
[Cuerpo: máximo 3 párrafos cortos]
- Párrafo 1: Acuse de recibo del problema, con referencia concreta
a lo que planteó.
- Párrafo 2: Resolución clara con pasos concretos y plazos si
aplican.
- Párrafo 3 (opcional): Información adicional relevante o próximos
pasos.
[Cierre cordial]
[Firma]
{{nombre_agente}}
{{canal_soporte}}
## Restricciones
- NO uses placeholders como [INSERTAR DATO] o {dato}. Si no tenés
un dato, omití esa parte.
- NO inventes números de seguimiento, fechas de entrega ni montos.
- NO ofrezcas descuentos o compensaciones salvo que la resolución
lo indique explícitamente.
- Extensión máxima: 150 palabras en el cuerpo del email.
- Idioma: español.
- Devolvé SOLO el email. Sin explicaciones, sin alternativas, sin
comentarios antes o después.
Ejemplo de uso concreto
## Contexto del caso
- **Tipo de reclamo**: envío
- **Nombre del cliente**: Mariana López
- **Número de pedido**: #58741
- **Resumen del problema**: El pedido figura como entregado pero
la clienta dice que nunca lo recibió.
- **Resolución a comunicar**: Se abrió investigación con el correo.
Plazo de respuesta: 72 horas hábiles. Si no se localiza el paquete,
se reenviará sin costo.
## Email original del cliente
Hola, hace 3 días que mi pedido figura como entregado y nunca llegó.
Ya revisé con mis vecinos y en portería y nada. Necesito una solución
porque es un regalo. Gracias, Mariana.
Salida esperada
Asunto: Pedido #58741 – Investigación de entrega en curso
Estimada Mariana:
Gracias por escribirnos. Lamento que el pedido #58741 no haya llegado
a pesar de figurar como entregado. Entiendo la urgencia, especialmente
tratándose de un regalo.
Ya abrimos una investigación con la empresa de correo para rastrear
el paquete. El plazo de respuesta es de 72 horas hábiles. Si no se
logra localizar, le reenviaremos el pedido sin costo adicional.
Voy a mantenerla informada apenas tenga novedades. Ante cualquier
consulta, no dude en responder este email.
Saludos cordiales,
[nombre_agente]
[canal_soporte]
Por qué funciona mejor
| Problema del original | Cómo lo resuelve el nuevo prompt | |---|---| | Sin rol definido | Rol explícito: agente de soporte de la empresa | | Sin contexto | Variables estructuradas con datos del caso | | Formato impredecible | Estructura de email obligatoria con párrafos definidos | | Tono inconsistente | Reglas de tono concretas con ejemplos de qué evitar | | Genera placeholders | Restricción explícita + regla de omisión si falta dato | | Inventa información | Prohibición de fabricar datos no provistos | | Respuesta con "ruido" | Instrucción de devolver SOLO el email |
Notas de implementación
Variables que tu sistema debe inyectar antes de enviar al LLM:
{{nombre_empresa}} → Desde config
{{nombre_cliente}} → Desde el ticket/CRM
{{numero_pedido}} → Desde el ticket/CRM
{{tipo_reclamo}} → Clasificación del ticket
{{resumen_problema}} → Del ticket o input del operador
{{resolucion}} → Input obligatorio del operador
{{email_cliente}} → Email original del cliente
{{registro_formal}} → Config por defecto o preferencia
{{nombre_agente}} → Sesión del operador
{{canal_soporte}} → Config
Crítico:
{{resolucion}}debe ser obligatorio. Si el operador no define qué comunicar, el LLM va a inventar una resolución. Ese es el campo que más reduce alucinaciones.
Criterios evaluados
Define rol, tarea, campos de entrada, restricciones anti-alucinación y límites de longitud con bastante detalle. Baja un poco por mezclar un prompt “sin placeholders” con un template lleno de variables tipo {{...}}, lo que puede causar fallas si no se preprocesa bien.
El prompt define rol explícito (agente de soporte de ecommerce), tarea concreta (redactar respuesta final al email), contexto estructurado con variables, restricciones detalladas y hasta una tabla comparativa que justifica cada decisión. Solo le falta quizás especificar manejo de edge cases como clientes agresivos o reclamos múltiples.
Propone un esquema claro de variables para inyectar contexto (tipo de reclamo, pedido, resumen, resolución, email original) y hasta lista de fuentes (CRM/config). Falta contemplar algunos casos frecuentes (estado de pago, tracking, dirección, evidencia, políticas locales) como campos opcionales estandarizados.
Estructura impecable con variables claramente nombradas usando doble llave, una tabla de implementación que mapea cada variable a su fuente (CRM, config, sesión del operador), y la nota crítica sobre hacer obligatorio el campo de resolución. Cubre todos los datos relevantes para los tres tipos de reclamo.
Especifica tono profesional y empático con pautas concretas, define registro (vos/usted) y un formato obligatorio (asunto, saludo, 3 párrafos, cierre, firma). Buen control de salida (‘devolvé SOLO el email’), aunque el ejemplo incumple su propia regla de “sin placeholders” en la firma.
Define tono profesional y empático con reglas concretas (evitar frases vacías sin acción, manejo de vos/usted con default), y el formato es muy preciso con estructura de párrafos obligatoria, límite de 150 palabras y firma. Podría haber incluido algún ejemplo adicional de frases a evitar o preferir para mayor control.
Muy cercano a production-ready: las variables están bien definidas con fuentes, hay notas de implementación claras y el ejemplo demuestra el flujo completo. Le resta que los placeholders de firma ([nombre_agente], [canal_soporte]) en la salida esperada contradicen la regla de no usar placeholders — debería aclarar que esos se inyectan post-generación o pre-envío. También faltaría mención de manejo de errores o edge cases (emails en otro idioma, insultos, etc.).
En general es utilizable y bien armado para un entorno de soporte, con buenas salvaguardas y campos. Sin embargo, hay inconsistencias (prohibición de placeholders vs variables/ejemplo), y faltan pautas de escalamiento cuando la “resolución” es insuficiente o cuando el cliente pide algo no contemplado.
Estructura fija, límites de extensión, prohibiciones de invención y un ejemplo ayudan a estandarizar salidas. Aun así, deja libertad considerable en redacción y no fuerza elementos lingüísticos específicos (p. ej., fórmulas de saludo/cierre concretas), por lo que la variabilidad no será mínima.
Estructura de email fija con párrafos definidos, restricciones explícitas (no placeholders, no inventar datos, solo el email sin comentarios), límite de extensión, ejemplo completo con input y output esperado. Todo esto reduce drásticamente la variabilidad entre ejecuciones.
Strategy
3 pruebas
S4.2gtm69.7
Respuesta del modelo
Estrategia GTM — 90 Días
Herramienta de Automatización de Reportes para Contadores
Primero, el diagnóstico que condiciona todo
Antes de la estrategia, hay que nombrar las 3 realidades que determinan cada decisión:
Realidad 1: El problema no es awareness, es activación. Tenés 8 pagos pero solo 6 activos. Eso es 25% de churn silencioso antes de que el producto tenga 90 días. Si eso no se arregla primero, cada cliente nuevo que entre tiene 1 en 4 chances de no arrancar nunca.
Realidad 2: Los founders son el cuello de botella. No hay equipo de ventas, no hay presupuesto. Cada hora invertida en un canal que no convierte es una hora que no se invierte en cerrar a alguien que ya levantó la mano. La estrategia tiene que ser brutalmente eficiente en tiempo de founder.
Realidad 3: El ciclo de compra corto es un arma de doble filo. Si compran el mismo día que ven la demo, también descartan el mismo día si la demo no fue convincente. Esto significa que la calidad de la primera impresión importa más que el volumen de impresiones.
Parte 1: Los 3 canales GTM principales
Canal 1: Alianzas con Colegios Profesionales y Asociaciones de Contadores
El canal, específicamente: No es "hacer networking". Es conseguir que el colegio profesional te dé un slot para presentar la herramienta a sus matriculados, ya sea en un webinar oficial, una nota en su newsletter, o una mención en su grupo de WhatsApp.
Por qué este canal y no otro:
| Factor | Evaluación | |---|---| | Costo | Prácticamente cero. Los colegios buscan contenido útil para sus matriculados | | Confianza transferida | Un contador que ve tu herramienta presentada por su colegio la evalúa con un sesgo de credibilidad que no podés comprar con ads | | Densidad de ICP | No estás hablando con "gente que podría ser contador". Estás hablando con 100% contadores matriculados activos | | Escala | Un solo webinar con un colegio provincial puede ponerte frente a 200-500 contadores en una hora |
El cálculo concreto: Argentina tiene 24 provincias, cada una con su Consejo Profesional de Ciencias Económicas. Si en 90 días conseguís presentar en 4-6 consejos, estás llegando a 1,000-3,000 contadores con cero gasto en ads y con la credibilidad institucional que un founder solo no tiene.
Cómo se ejecuta (el mecanismo, no el calendario):
- Identificar los 10 consejos profesionales con mayor actividad digital (tienen newsletter activa, hacen webinars, tienen grupo de WhatsApp o Telegram)
- Preparar una propuesta de valor para el colegio, no para el contador. El pitch es: "Quiero dar una capacitación gratuita sobre automatización de reportes para sus matriculados. Dura 45 minutos. No cobro nada. Ustedes dan valor a sus miembros."
- El formato ideal: webinar de 30 min donde los primeros 20 min son contenido real (cómo automatizar X proceso) y los últimos 10 min son demo en vivo
- Cerrar con una oferta exclusiva para miembros del colegio (primer mes gratis, onboarding personalizado)
Riesgo principal: Los colegios son burocráticos. Puede tomar 3-6 semanas desde el primer contacto hasta la fecha del webinar. Por eso hay que empezar el día 1.
Canal 2: Programa de Referidos Estructurado con los 6 Contadores Activos
El canal, específicamente: Convertir a los 6 usuarios activos en un canal de adquisición sistemático, no con un "si conocés a alguien pasale mi contacto" sino con un programa con incentivos claros, materiales listos y seguimiento.
Por qué este canal y no otro:
La evidencia más fuerte que tenés hoy no es tu landing page ni tu pitch deck. Es que 6 contadores están usando el producto todos los meses y tienen un NPS de 72. Esos 6 contadores hablan con otros contadores todos los días. En grupos de WhatsApp de contadores, en reuniones de consejo, en almuerzos. El boca a boca ya está pasando de forma desordenada (los 3 inbound de México probablemente vienen de ahí). La pregunta es si lo dejás al azar o lo sistematizás.
El cálculo concreto:
- 6 contadores activos
- Cada contador conoce, conservadoramente, 15-20 colegas con los que tiene relación suficiente para recomendar algo
- Si cada uno refiere 2-3 contadores en 90 días (tasa conservadora para un programa bien ejecutado), son 12-18 leads calificados
- Con el ciclo de compra corto que describís, la conversión de referido a pago debería estar en 40-60%
- Resultado esperado: 5-11 clientes nuevos solo de este canal
Cómo se ejecuta:
Estructura del programa:
Por cada contador que refieras y se active (cargue al menos 1 cliente):
→ 1 mes gratis por cada 5 clientes que cargue tu referido
→ Si referís 3+ contadores que se activen: precio congelado de por vida
Para el referido:
→ Primer mes gratis
→ Onboarding 1:1 con un founder (esto es valioso, usarlo)
Materiales que necesitan existir:
- Un mensaje de WhatsApp pre-escrito que el contador pueda reenviar (no un link frío, un mensaje que suene a recomendación personal)
- Un video de 2 minutos donde UN CONTADOR REAL (no un founder) explica qué le resuelve la herramienta
- Un link de referido trackeable para que el contador vea cuántos se registraron por él
Por qué incentivos económicos y no solo "buena onda": Un NPS de 72 es bueno pero no es excepcional. No todos los usuarios con NPS 72 refieren espontáneamente. El incentivo económico no es para convencerlos de que el producto es bueno (ya lo saben), es para que la referencia pase de "debería contarle a alguien" a "lo voy a hacer hoy".
Riesgo principal: La base es pequeña. Si 2 de los 6 no están interesados en referir, el canal se achica rápido. Por eso no puede ser el único canal.
Canal 3: Contenido en LinkedIn orientado a un único formato que funciona para este ICP
El canal, específicamente: No es "hacer contenido en LinkedIn". Es publicar 3-4 veces por semana con UN SOLO TIPO de contenido: posts que muestren el antes/después concreto de un proceso que el contador hace manualmente hoy vs. cómo lo hace con la herramienta.
Por qué este canal y no otro:
| Pregunta | Respuesta | |---|---| | ¿Los contadores están en LinkedIn? | Sí. Es la red profesional donde los contadores argentinos y mexicanos tienen presencia activa. Los colegios profesionales publican ahí. | | ¿El formato funciona para este producto? | Sí, porque el producto tiene un antes/después visual muy claro. "Esto me tomaba 3 horas → ahora toma 4 minutos" es el tipo de contenido que genera engagement orgánico | | ¿Se puede hacer sin presupuesto? | Sí. Un founder con conocimiento del producto puede producir este contenido en 30-45 min/día | | ¿Escala sin equipo? | Parcialmente. El alcance orgánico de LinkedIn tiene techo, pero para los primeros 90 días es suficiente |
Por qué LinkedIn y no Twitter/X, YouTube, o un blog:
- Twitter/X: Los contadores latinoamericanos no discuten su profesión en Twitter. La densidad de ICP es bajísima.
- YouTube: Requiere producción. Un video decente toma 4-8 horas entre grabación, edición y publicación. Los founders no tienen ese tiempo.
- Blog/SEO: El SEO toma 4-6 meses en dar resultados. No entra en una ventana de 90 días.
- LinkedIn: Combina presencia del ICP + bajo costo de producción + resultados en días, no meses.
El formato específico que hay que usar (y los que hay que evitar):
✅ SÍ usar:
- Screenshots del antes (Excel caótico, proceso manual) vs. después (reporte generado automáticamente)
- Posts cortos (< 200 palabras) que cuenten una situación real de un contador usuario: "María maneja 45 clientes. Cada mes dedicaba el primer lunes completo a generar reportes de AFIP. Ahora le llegan al mail el viernes anterior."
- Carruseles de 5-7 slides mostrando el flujo: "Cómo automatizar tus reportes de AFIP en 10 minutos"
- Encuestas: "¿Cuántas horas al mes dedicás a generar reportes para tus clientes?" (genera engagement Y data)
❌ NO usar:
- Posts motivacionales genéricos sobre emprendimiento
- Contenido sobre "el futuro de la contabilidad" o "IA en finanzas" (demasiado abstracto, no convierte)
- Posts largos tipo ensayo (los contadores scrollean rápido)
Riesgo principal: LinkedIn orgánico tiene alcance decreciente. Si no se combina con interacción genuina en los posts de otros contadores y en grupos, el alcance muere en la semana 3.
Por qué estos 3 canales juntos y no individualmente
Colegios Profesionales → Genera volumen de awareness calificado
(top of funnel, 200-500 contadores por evento)
↓
LinkedIn → Mantiene presencia entre eventos
(middle of funnel, nurturing continuo)
↓
Referidos → Convierte con la mayor tasa posible
(bottom of funnel, confianza peer-to-peer)
Los tres canales se refuerzan: un contador ve la herramienta en un webinar del colegio, después ve un post en LinkedIn, después un colega se la recomienda. Tres touchpoints de alta credibilidad, cero pesos en ads.
Parte 2: Plan semana a semana — Primeros 30 días
Semana 0 (pre-lanzamiento del GTM, días 1-3)
Antes de hacer cualquier cosa de adquisición, resolver la activación:
| Acción | Detalle | Responsable | Entregable | |---|---|---|---| | Llamar a los 2 contadores inactivos | Llamada de 15 min. No para vender, para entender. ¿Qué pasó? ¿No entendieron algo? ¿El producto no resuelve su caso? ¿Se olvidaron? | Founder 1 | Diagnóstico escrito de por qué no activaron | | Definir qué es "activación" | Un contador está activo cuando cargó al menos 3 clientes y generó 1 reporte automático. Si no pasó eso, no está activo. | Ambos founders | Definición documentada | | Crear la secuencia de onboarding | Emails/WhatsApp automáticos para los primeros 7 días de un usuario nuevo: Día 1: bienvenida + video de setup. Día 3: "¿Cargaste tu primer cliente?". Día 7: "Tu primer reporte está listo" o "¿Necesitás ayuda?" | Founder 2 | Secuencia lista en la herramienta que usen (Mailchimp, WhatsApp Business, lo que sea) |
Por qué esto primero: Si no arreglás la activación antes de meter más gente al funnel, vas a tener 16 contadores pagos con 12 activos en vez de 8 con 6. El ratio no mejora, solo gastás más tiempo.
Semana 1 (días 4-10)
Tema: Armar la infraestructura del GTM
Lunes-Martes: Programa de referidos
- [ ] Diseñar la estructura de incentivos (descrita arriba)
- [ ] Crear el link de referido trackeable (puede ser algo tan simple como un UTM + formulario dedicado)
- [ ] Escribir el mensaje de WhatsApp pre-escrito para que los contadores reenvíen
- [ ] Grabar el video testimonial con 1-2 de los contadores más activos (puede ser por Zoom, no necesita producción profesional. 2 minutos máximo.)
Miércoles: Lanzar el programa con los 6 activos
- [ ] Llamar uno por uno (no mandar un mail masivo). Explicarles el programa, preguntarles si tienen colegas que podrían beneficiarse
- [ ] Enviar los materiales por WhatsApp inmediatamente después de la llamada
- [ ] Preguntarles: "¿En qué grupos de WhatsApp de contadores estás?" (esto es inteligencia de mercado para el Canal 1)
Jueves-Viernes: Colegios profesionales
- [ ] Mapear los 10 Consejos Profesionales de Ciencias Económicas más activos digitalmente en Argentina
- [ ] Encontrar el contacto de la persona que organiza capacitaciones/eventos en cada uno (LinkedIn, web del consejo, llamada telefónica)
- [ ] Enviar la propuesta a los primeros 5 (email personalizado, no template genérico)
Sábado-Domingo: LinkedIn
- [ ] Optimizar los perfiles de LinkedIn de ambos founders (headline que diga qué hacen para contadores, no "CEO de [startup]")
- [ ] Preparar el calendario de contenido de las semanas 2-4 (12 posts)
- [ ] Escribir y programar los primeros 3 posts
Semana 2 (días 11-17)
Tema: Primeras acciones públicas + México
LinkedIn — publicar 4 posts esta semana:
- Lunes: Post de antes/después (screenshot de proceso manual vs. automático)
- Miércoles: Encuesta: "¿Cuántas horas al mes dedicás a reportes de AFIP/SAT?"
- Viernes: Carrusel: "5 reportes que podés automatizar hoy si sos contador"
- Domingo: Historia personal del founder: "Por qué decidimos construir esto" (humaniza, genera conexión)
Después de cada post: Dedicar 20 minutos a comentar en posts de contadores, responder comentarios, conectar con contadores que interactuaron.
México — activar los 3 leads inbound:
- [ ] Llamar/videollamar a cada uno. Objetivo: entender su contexto, hacer demo en vivo, cerrar si
Criterios evaluados
Los 3 canales (colegios/asociaciones, referidos desde usuarios activos, LinkedIn orgánico) están bien alineados con ICP (contadores), restricción de cero ads y ciclo de compra corto. Falta explotar explícitamente WhatsApp/comunidades como canal primario (más allá de referidos) y el plan es muy Argentina-céntrico para un GTM binacional.
Los tres canales elegidos (colegios profesionales, referidos de usuarios activos, LinkedIn orgánico) son excelentes para este contexto: cero presupuesto, ICP agrupado en comunidades profesionales, ciclo de compra corto. La justificación de por qué NO usar Twitter, YouTube o SEO es precisa y basada en datos del caso. El único gap menor es que no profundiza lo suficiente en WhatsApp como canal directo (lo menciona como inteligencia de mercado pero no como canal de distribución propio), siendo que el brief lo señala como canal clave de las comunidades de contadores.
Las semanas 0-2 están bastante accionables para 2 founders (activación, referidos, outreach a consejos, cadencia de LinkedIn) y aprovechan NPS/usuarios activos. Pero el plan está incompleto/cortado (no llega a semanas 3-4), y algunas tareas carecen de plantillas/volúmenes objetivo (p.ej., cuántos mensajes/demos por semana).
Las semanas 0-2 están muy bien detalladas con acciones específicas, checkboxes, responsables y entregables. La 'Semana 0' de resolver activación antes de adquirir es un insight estratégico excelente. Sin embargo, la respuesta se corta abruptamente en la Semana 2 (el plan de México queda incompleto), faltando las semanas 3 y 4 que se pidieron explícitamente. Solo cubre ~17 de los 30 días solicitados, lo cual es una omisión significativa.
No entrega un set de métricas con objetivos numéricos claros; solo hay algunos números sueltos/estimaciones (p.ej., 40-60% conversión de referidos) sin un cuadro de KPIs, targets por canal y umbrales de éxito/fracaso en 30-90 días.
Esta es la debilidad más grave de la respuesta. La tarea pide explícitamente 'métricas que van a indicar si el GTM está funcionando (con números objetivo)' y la respuesta no incluye una sección dedicada a métricas. Hay algunos números dispersos (5-11 clientes de referidos, 1000-3000 contadores alcanzados), pero no hay una tabla de KPIs con targets concretos por canal, por período, ni umbrales de decisión. Falta completamente.
Justifica bien por qué estos canales y descarta otros (Twitter, YouTube, SEO) con argumentos razonables de densidad de ICP y time-to-value, y usa datos como NPS y ciclo corto. Sin embargo, no cumple el requerimiento explícito de 'el canal que NO usarían' como sección clara y falta profundizar trade-offs específicos (burocracia de colegios vs. velocidad, enfoque México vs. AR).
La fundamentación es sólida y usa los datos del brief de forma inteligente: el cálculo de ROI de referidos (6 activos × 2-3 referidos × 40-60% conversión), la identificación del problema de activación (25% churn silencioso), la explicación de por qué cada canal alternativo no aplica. El diagnóstico inicial de las '3 realidades' demuestra pensamiento estratégico genuino. Falta la sección explícita de 'canal que NO usarían' (entregable 4), aunque hay justificaciones parciales distribuidas en el texto sobre Twitter, YouTube y SEO.
S4.3pricing81.7
Respuesta del modelo
Rediseño de Pricing — Plataforma de Gestión de Turnos para Salud
1. Diagnóstico: Por qué el pricing actual está destruyendo valor
El problema central: un precio único para dos (o tres) perfiles radicalmente distintos
Tu base de clientes tiene una distribución bimodal que estás tratando con un precio uniforme. Esto genera un doble problema simultáneo:
El profesional solo que atiende 50 pacientes:
- Paga $49/mes por features que no usa (multiusuario, reportes de ocupación avanzados)
- Su costo real de WhatsApp es ~$1/mes (50 recordatorios)
- Tu margen en este cliente es alto (~80%), pero él percibe que paga de más
- Compara con Agenda Pro a $29 y siente que sobrepaga un 70%
- Este es el 60% que dice "es caro para lo que uso" + el 25% que dice "no uso todo"
- Estos dos grupos son el mismo perfil. Representan el ~85% de tu churn
El consultorio con 3+ profesionales y +500 mensajes:
- Paga los mismos $49/mes pero extrae 3-5x más valor
- Su costo real de WhatsApp es ~$10/mes, más carga en infraestructura
- Tu margen en este cliente es bajo (~40-45%), pero él percibe que es barato
- No tiene alternativa buena: Agenda Pro no le alcanza, MediTurno le queda grande
- Estos clientes no se van. Están subsidiados por los que sí se van
El ciclo vicioso actual
Profesional solo entra → Paga $49 → Usa 30% del producto
→ Se siente estafado → Se va a los 3-4 meses
→ Perdés el cliente + el CAC invertido
→ Necesitás adquirir más clientes para compensar
→ El 8% de churn te obliga a crecer solo para no caer
Dato clave que confirma esto: con 8% de churn mensual, estás perdiendo ~25 clientes/mes. Para mantener 320 necesitás adquirir 25 nuevos cada mes. Si tu CAC es de $100-150 (típico B2B SaaS en LATAM), estás quemando $2,500-$3,750/mes solo para quedarte en el mismo lugar.
El churn no es un problema de retención, es un problema de pricing-product fit
No necesitás mejorar onboarding ni agregar features. Necesitás que cada cliente pague un precio proporcional al valor que recibe.
2. Propuesta de estructura de pricing
Arquitectura: 3 tiers + componente variable
La lógica es: tier por complejidad del consultorio, variable por uso de WhatsApp.
| | Solo | Consultorio | Clínica | |---|---|---|---| | Precio | $19/mes | $49/mes | $99/mes | | Profesionales incluidos | 1 | Hasta 4 | Hasta 12 | | Usuarios (recepcionistas, admin) | 1 | 3 | Ilimitados | | Recordatorios WhatsApp incluidos | 100/mes | 500/mes | 2,000/mes | | WhatsApp adicional | $0.05 c/u | $0.04 c/u | $0.03 c/u | | Agenda online | ✅ | ✅ | ✅ | | Historial de pacientes | ✅ | ✅ | ✅ | | Recordatorios automáticos | ✅ | ✅ | ✅ | | Reportes de ocupación | Básico (vista semanal) | Completo | Completo + exportable | | Personalización de marca | ❌ | Logo propio | Dominio propio + branding | | Soporte | Email (48hs) | Email + chat (24hs) | Prioritario + onboarding dedicado | | API / Integraciones | ❌ | Webhook básico | API completa |
Componente variable: WhatsApp como métrica de expansión
El WhatsApp incluido en cada plan cubre el uso típico del perfil. El excedente tiene markup (costo real $0.02, cobro $0.03-0.05), lo que:
- Protege tu margen en clientes de alto uso
- Crea un upgrade natural: cuando un cliente del plan Solo empieza a crecer y supera 100 mensajes, el plan Consultorio se vuelve más conveniente
- Genera revenue de expansión sin fricción
3. Justificación tier por tier
Tier 1: Solo — $19/mes
¿A quién apunta? Al profesional independiente: el psicólogo con 30-60 pacientes activos, el dentista que recién arranca, el médico que atiende solo en un consultorio alquilado por horas.
¿Por qué $19?
| Factor | Razonamiento | |---|---| | Competencia | $10 menos que Agenda Pro ($29), pero con WhatsApp incluido. Esto es un diferenciador letal: les das algo que el competidor no tiene, a menor precio. | | Percepción de valor | Un profesional con 50 pacientes factura típicamente $2,000-5,000/mes. $19 es <1% de sus ingresos. La barrera psicológica desaparece. | | Unit economics | Costo infra $8 + WhatsApp ~$2 (100 msgs) = $10. Margen bruto: $9/cliente (47%). Es menor que hoy, pero estos clientes hoy se van. Un cliente que paga $19 durante 18 meses vale más que uno que paga $49 durante 3. | | Rol estratégico | Es tu funnel de entrada. Estos profesionales crecen, contratan colegas, abren consultorios. Los querés dentro del ecosistema. |
¿Qué limita este tier?
- 1 profesional, 1 usuario: si contrata recepcionista, necesita Consultorio
- 100 mensajes WhatsApp: suficiente para ~50-80 pacientes activos con recordatorios
- Reportes básicos: suficiente para gestión personal, insuficiente para gestión de negocio
- Sin integraciones: no necesita, pero cuando crezca, sí
Métrica de upgrade natural: Cuando contrata un colega o recepcionista → Consultorio.
Tier 2: Consultorio — $49/mes
¿A quién apunta? Al consultorio con 2-4 profesionales: el consultorio odontológico con 2 dentistas y una recepcionista, el centro de psicología con 3 terapeutas, el consultorio médico con un titular y un asociado.
¿Por qué $49?
| Factor | Razonamiento | |---|---| | Anclaje | Es tu precio actual. Los clientes actuales que usan el producto moderadamente no ven cambio de precio, pero ahora el tier está diseñado para ellos y se sienten en el lugar correcto. | | Valor percibido | Un consultorio con 3 profesionales factura $8,000-20,000/mes. $49 es imperceptible. Y ahora tienen features que justifican el precio: multiusuario, reportes completos, más WhatsApp. | | Unit economics | Costo infra $8 + WhatsApp ~$10 (500 msgs) = $18. Margen bruto: $31/cliente (63%). Idéntico margen porcentual al actual, pero en un cliente que percibe justo el precio. | | Competencia | MediTurno cobra $89 por algo similar pero orientado a clínicas grandes. Estás en el sweet spot: más completo que Agenda Pro, más accesible que MediTurno. |
¿Qué limita este tier?
- 4 profesionales: cubre el consultorio típico, no la clínica
- 500 mensajes: suficiente para ~200-350 pacientes activos
- Sin API completa: suficiente para operación, insuficiente para integración con sistemas de clínica
- Soporte estándar: no dedicado
Métrica de upgrade natural: Cuando supera 4 profesionales o necesita integraciones con sistema contable/médico → Clínica.
Tier 3: Clínica — $99/mes
¿A quién apunta? A la clínica o centro médico con 5-12 profesionales: la clínica dental con múltiples sillas, el centro de salud con varias especialidades, el policonsultorio.
¿Por qué $99?
| Factor | Razonamiento | |---|---| | Valor capturado | Una clínica con 8 profesionales factura $30,000-80,000/mes. $99 es una fracción insignificante. Y el costo de no gestionar bien los turnos (no-shows, agenda vacía) es de miles por mes. | | Competencia | $10 más barato que MediTurno ($89... espera, es más caro). Pero incluís 2,000 mensajes de WhatsApp que MediTurno no tiene. El TCO real es menor. | | Unit economics | Costo infra $8 (podría ser ~$12 por mayor uso) + WhatsApp ~$40 (2,000 msgs) = $50-52. Margen bruto: $47-49/cliente (~48%). Menor margen porcentual, pero $47 absolutos por cliente. Y el variable de WhatsApp adicional protege el downside. | | Posicionamiento | Demuestra que tu producto escala. Sin este tier, las clínicas nunca te consideran. Con él, competís con MediTurno sin necesidad de construir features enterprise. |
¿Qué limita este tier?
- 12 profesionales: por encima de eso, necesitás un plan Enterprise/custom (futuro)
- 2,000 mensajes: suficiente para ~800-1,200 pacientes activos
- El precio del WhatsApp adicional a $0.03 (vs $0.02 de costo) da margen pero es competitivo
Métrica de upgrade natural: Cuando supera 12 profesionales → conversación de venta Enterprise (pricing custom, SLA, implementación).
Resumen de la lógica de diferenciación
SOLO CONSULTORIO CLÍNICA
$19/mes $49/mes $99/mes
│ │ │
Eje principal: Nº de profesionales y usuarios
1 prof 2-4 prof 5-12 prof
│ │ │
Eje secundario: Volumen de WhatsApp (proxy de actividad)
100 msgs 500 msgs 2,000 msgs
│ │ │
Eje terciario: Sofisticación operativa
Individual Equipo chico Gestión compleja
¿Por qué estos dos ejes y no otros?
- Nº de profesionales es observable, no manipulable, y correlaciona directamente con el valor que extraen y con su capacidad de pago.
- WhatsApp es el feature más valorado (diferenciador vs competencia) y tiene costo variable real. Usarlo como métrica alinea valor percibido, costo y revenue.
- No uso "número de pacientes" porque es difícil de auditar y genera fricción ("¿cuento pacientes activos o totales?").
4. Impacto estimado en MRR
Supuestos de migración (escenario: 50% migra al tier adecuado)
Basado en los datos que compartiste:
| Segmento | % de base | Clientes | Tier actual | Tier natural | Migración (50%) | |---|---|---|---|---|---| | Bottom 30% (solos, <50 pacientes) | 30% | 96 | $49 | Solo $19 | 48 migran a $19 | | Medio 50% (uso moderado) | 50% | 160 | $49 | Consultorio $49 | 80 se quedan en $49 | | Top 20% (3+ profs, +500 msgs) | 20% | 64 | $49 | Clínica $99 | 32 migran a $99 |
El otro 50% que no migra inmediatamente se queda en $49 (lo mantenés como plan legacy durante 3-6 meses para reducir fricción).
Cálculo de MRR post-migración inmediata
Clientes que migran a Solo: 48 × $19 = $912
Clientes que migran a Consultorio: 80 × $49 = $3,920
Clientes que migran a Clínica: 32 × $99 = $3,168
Clientes que no migran (legacy): 160 × $49 = $7,840
─────────
MRR inmediato: $15,840
Cambio inmediato en MRR: +$160 (+1%). Prácticamente neutro.
Esto es esperable y correcto. El objetivo del repricing no es subir MRR el día 1. Es reducir churn y habilitar expansión.
El impacto real: reducción de churn
Escenario conservador de churn por tier:
| Tier | Churn actual | Churn esperado | Justificación | |---|---|---|---| | Solo ($19) | ~12-15% (este grupo era el que más se iba) | 5-6% | Precio justo, sin alternativa mejor con WhatsApp | | Consultorio ($49) | ~6-8% | 3-4% | Precio igual pero ahora con features que justifican | | Clínica ($99) | ~2-3% (ya casi no se iban) | 1-2% | Más valor, más lock-in, switching cost alto | | Promedio ponderado | 8% | ~3.5-4% | |
Proyección a 12 meses: MRR con churn reducido
Escenario actual (sin cambios):
MRR mes 0: $15,680
Churn 8%/mes, asumiendo 25 nuevos clientes/mes a $49
Mes 12 MRR: ~$15,200-16,000 (estancado, corriendo para quedarte en el mismo lugar)
Escenario nuevo pricing:
MRR mes 0: $15,840
Churn ~3.5%/mes
Nuevos clientes: 25/mes (mismo ritmo)
- Distribución esperada de nuevos: 50% Solo, 35% Consultorio, 15% Clínica
- ARPU nuevos: (0.5×$19 + 0.35×$49 + 0.15×$99) =
Criterios evaluados
Hace un cálculo concreto para 50% de migración usando 320 clientes y segmentación 30/50/20, y muestra el MRR resultante. Pero asume una distribución de segmentos y un “tier natural” para el 20% top que no está totalmente soportado (top 20% tiene 3 profesionales, pero el tier Clínica está pensado 5-12), no discute riesgo de downgrade/cancelación en migración y deja la proyección anual inconclusa.
Hace una estimación concreta y bien estructurada con supuestos explícitos: segmenta los 320 clientes en tres grupos basados en los datos provistos, calcula el MRR inmediato ($15,840, +1%), y es honesto sobre que el impacto día 1 es neutro. La proyección de reducción de churn por tier es razonable y bien justificada. Sin embargo, la proyección a 12 meses queda incompleta (se corta a mitad del cálculo del ARPU de nuevos clientes), lo cual es una falla significativa en la entrega. También falta análisis explícito del riesgo de pérdida de clientes durante la migración.
La estructura 3 tiers + variable por WhatsApp está bien alineada con segmentos (nº de profesionales) y con el costo variable (mensajes), y posiciona razonablemente vs $29 y $89. Hay inconsistencias/errores (comparación con MediTurno en tier Clínica, mezcla de supuestos de infra por mayor uso) y el tier “Solo $19” deja margen bajo frente al costo fijo $8 + WA, elevando riesgo si el uso real supera lo incluido.
Estructura muy coherente con 3 tiers bien diferenciados. La métrica principal (nº profesionales) es observable y no manipulable, el WhatsApp como eje secundario alinea costos con revenue. Los precios están bien posicionados frente a competencia ($19 < $29 Agenda Pro, $99 vs $89 MediTurno con WhatsApp incluido). La justificación de por qué no usar 'número de pacientes' es un detalle excelente. Pequeña inconsistencia: menciona que $99 es más caro que MediTurno ($89) y se autocorrige torpemente, y el margen del tier Clínica (~48%) podría ser preocupante a escala. El componente variable de WhatsApp adicional está bien diseñado como mecanismo de expansión.
Fundamenta gran parte en los datos del caso (razones de churn, segmentación por uso, costos WhatsApp e infraestructura, competencia) y traduce eso en métricas de diferenciación. Aun así, introduce números y conclusiones no provistas (CAC, tasas de churn esperadas por tier) y comete una inconsistencia de pricing competitivo, lo que debilita la solidez.
Prácticamente cada decisión está fundamentada en datos del caso: los precios de competencia, los costos de WhatsApp, la distribución de uso, las razones de churn. Las tablas de justificación por tier con factores (competencia, percepción, unit economics, posicionamiento) son específicas al caso. La explicación de por qué usa 'número de profesionales' y no 'número de pacientes' como métrica demuestra pensamiento profundo sobre el diseño. No hay recomendaciones genéricas de SaaS desconectadas del contexto.
Identifica bien el problema one-size-fits-all y conecta explícitamente el churn (“caro para lo que uso / no uso todo”) con los clientes pequeños, y el top 20% con alto valor y alto uso. También reconoce WhatsApp como costo variable clave, aunque algunas cifras de margen por segmento están poco sustentadas.
Diagnóstico excelente: identifica con precisión el problema one-size-fits-all, conecta el 60%+25% de churn reasons con el perfil de clientes pequeños (insight brillante de que son el mismo grupo), cuantifica el ciclo vicioso del churn con CAC, y reconoce el costo variable de WhatsApp como factor crítico. La observación de que 'un cliente que paga $19 durante 18 meses vale más que uno que paga $49 durante 3' demuestra comprensión profunda del problema.
S4.1roadmap82.7
Respuesta del modelo
Roadmap 6 Meses — Plataforma AI Agents para PyMEs LATAM
Diagnóstico previo al roadmap
Antes de planificar, necesito nombrar el elefante en la sala: con $50k totales (incluyendo sueldos) y 2.5 personas técnicas, no pueden competir en features contra Treble.ai. Pueden competir en tres cosas: velocidad de onboarding, calidad de respuesta para nichos específicos, y cercanía con el cliente. El roadmap entero está diseñado alrededor de eso.
El otro problema estructural: los founders dedican solo 30% a ventas y necesitan llegar a 50 clientes pagos. Eso tiene que subir a 50%+ a partir del mes 3, lo cual implica que el producto tiene que necesitar menos intervención manual de ellos.
MES 1: Matar el churn arreglando el onboarding
El problema más urgente no es el AI agent, es que el 40% se va porque no puede ni configurarlo.
Entregables
1. Wizard de onboarding guiado (prioridad máxima)
- Flujo paso a paso: conectar WhatsApp → subir info del negocio → probar el agente → publicar
- Máximo 5 pasos, con preview en vivo de cómo responde el agente en cada paso
- Templates pre-armados para los 3 verticales actuales (contadores, abogados, clínicas)
2. Cambiar el input de "texto plano" a algo que la gente entienda
- En lugar de pedir un documento, hacer un formulario conversacional: "¿Cuáles son tus servicios?", "¿Cuáles son tus horarios?", "¿Cuánto cobras la consulta?"
- Esto resuelve dos problemas: el cliente no tiene que redactar nada, y la base de conocimiento queda estructurada (lo que mejora las respuestas del agente)
3. Sandbox de prueba pre-deploy
- Simulador de WhatsApp dentro de la plataforma donde el cliente puede testear su agente antes de conectarlo al número real
- Esto reduce el miedo a "quedar mal con mis clientes"
Justificación
Si el onboarding no baja de 2+ horas a <30 minutos, nada de lo que construyan después importa. No van a retener clientes ni conseguir referidos. El 40% de churn probablemente no es por la calidad del AI — es gente que nunca logró que funcione bien.
Asignación de equipo
- Dev 1: wizard de onboarding + formulario estructurado
- Dev 2: sandbox de prueba + refactor del pipeline de ingesta de conocimiento
- UX (20hs): diseño completo del wizard, testing con 3 clientes beta actuales
Métrica de éxito
- Tiempo de onboarding < 30 min (medido con los 5 clientes beta, pedirles que re-hagan el setup)
- Al menos 3 de 5 clientes beta completan el setup sin ayuda
MES 2: Arreglar la inteligencia del agente
Ahora sí: el agente tiene que dejar de fallar en preguntas ambiguas.
Entregables
1. Memoria conversacional (contexto de conversaciones anteriores)
- Implementar ventana de contexto: el agente recuerda los últimos 10 mensajes de la conversación actual
- Almacenar resúmenes de conversaciones pasadas por contacto (no el historial completo, que es caro en tokens)
- Esto resuelve el problema #1 reportado en beta
2. Manejo de ambigüedad con preguntas clarificadoras
- Cuando el agente no tiene confianza suficiente en la respuesta (threshold configurable), en lugar de inventar, pregunta: "¿Te referís a X o a Y?"
- Fallback elegante: "No tengo esa información, pero puedo conectarte con [nombre del dueño]" con botón de derivación a humano
3. Derivación a humano vía WhatsApp
- Botón/keyword para que el usuario pida hablar con una persona
- Notificación al dueño del negocio por WhatsApp cuando hay derivación
- Esto es crítico: las PyMEs necesitan saber que no van a perder clientes por una respuesta mala del bot
Justificación
La memoria conversacional y el manejo de ambigüedad son lo que separa un "bot de FAQ" de algo que se siente como soporte real. Sin esto, los clientes de sus clientes se frustran, el dueño de la PyME recibe quejas, y cancela. La derivación a humano es el safety net que permite a las PyMEs confiar en el agente.
Decisión técnica importante
No construyan su propio framework de memoria. Usen el approach más simple que funcione:
- Conversación actual: pasar los últimos N mensajes como contexto en el prompt
- Historial por contacto: resumen generado por LLM al cierre de cada conversación, almacenado en un campo de texto por contacto
- Esto cuesta ~$0.01-0.03 adicional por conversación en tokens, manejable
Asignación de equipo
- Dev 1: memoria conversacional + storage de resúmenes
- Dev 2: lógica de ambigüedad/clarificación + flujo de derivación humana
- UX (20hs): diseño de la experiencia de derivación, cómo se ve la notificación al dueño
Métrica de éxito
- Tasa de respuestas "no sé" o inventadas baja >50% (medido con logs de los beta)
- Churn mensual baja a <25%
- Los 5 clientes beta reportan mejora cualitativa
MES 3: Lanzamiento comercial + pricing
El producto ya es usable. Es hora de cobrar y escalar de 5 a 15-20 clientes.
Entregables
1. Sistema de billing y planes
- Integración con Stripe (soporta cobros en LATAM) o MercadoPago
- Plan único inicial: $149/mes (no complicar con tiers todavía)
- Trial de 14 días con onboarding completo (no freemium — el freemium mata a empresas con este budget)
2. Dashboard de métricas para el cliente
- Conversaciones atendidas por el agente vs. derivadas a humano
- Preguntas más frecuentes (esto le muestra al dueño el valor: "tu agente respondió 347 consultas este mes")
- Satisfacción básica: al final de la conversación, "¿Te fue útil? 👍👎"
3. Landing page + flujo de signup self-service
- Landing enfocada en los 3 verticales (contadores, abogados, clínicas)
- Testimonio de al menos 2 clientes beta
- CTA directo a trial de 14 días
4. Playbook de ventas para founders
- Script para llamada/WhatsApp de venta
- Secuencia de follow-up automatizada (pueden usar su propio producto para esto, que es buen dogfooding)
- Los founders suben a 50% de tiempo en ventas
Justificación
El mes 3 es el punto de inflexión. Tienen un producto que se puede configurar solo y que responde razonablemente bien. Cada mes que pasan sin cobrar es runway quemado. El dashboard de métricas es lo que justifica el pago mensual — el dueño de la PyME necesita ver el ROI.
Sobre el pricing de $149/mes: es correcto para el mercado. Un contador en Argentina cobra $50-100k ARS por cliente. Si el agente le ahorra 2 horas diarias de responder WhatsApps, $149 USD es barato. No bajen el precio por miedo.
Asignación de equipo
- Dev 1: billing + dashboard de métricas
- Dev 2: landing + flujo self-service + trial
- UX (20hs): dashboard, landing page
Founders
- Convertir los 5 beta a pagos (objetivo: 3-4 conviertan)
- Empezar outbound en las 3 verticales en Argentina
- Pedir referidos a cada cliente beta (ofrecer 1 mes gratis por referido que convierta)
Métrica de éxito
- 15-20 clientes pagos al final del mes
- Churn < 20%
- Onboarding self-service funciona sin intervención del equipo en >70% de los casos
MES 4: Retención profunda + expansión vertical
El crecimiento sin retención es llenar un balde con agujeros. Este mes es sobre hacer que los clientes no puedan irse.
Entregables
1. Entrenamiento continuo del agente por el cliente
- Cuando el agente deriva a humano, capturar la respuesta que dio el humano y sugerirla como nuevo conocimiento: "¿Querés que el agente aprenda esta respuesta para la próxima?"
- Esto es el flywheel: cuanto más usa el cliente el producto, mejor se pone el agente, más difícil es irse
2. Templates verticales mejorados
- Basándose en datos reales de los clientes actuales, crear bases de conocimiento pre-armadas mucho más completas para cada vertical
- Incluir flujos específicos: para clínicas → agendar turnos, para contadores → recordatorio de vencimientos, para abogados → intake de caso nuevo
3. Respuestas con acciones (no solo texto)
- El agente puede enviar links, archivos, ubicación
- Botones de respuesta rápida de WhatsApp (listas, botones interactivos)
- Esto sube dramáticamente la utilidad percibida
4. Alertas proactivas al dueño
- "Tuviste 15 consultas sobre [tema X] esta semana y tu agente no supo responder 8. ¿Querés agregar información sobre eso?"
- Esto educa al cliente sobre cómo mejorar su agente y lo mantiene engaged
Justificación
El entrenamiento continuo es la feature más importante de retención a largo plazo. Crea switching costs reales: si un cliente lleva 3 meses enseñándole cosas a su agente, no va a empezar de cero en otra plataforma. Las alertas proactivas atacan el problema de clientes que configuran y se olvidan (y después cancelan porque "no sirve").
Asignación de equipo
- Dev 1: entrenamiento continuo + alertas proactivas
- Dev 2: botones interactivos WhatsApp + respuestas con acciones
- UX (20hs): flujo de "aprender respuesta", diseño de alertas
Métrica de éxito
- Churn baja a <15%
-
50% de clientes usan la función de "enseñar" al agente al menos 1 vez por semana
- NPS > 40
MES 5: Motor de crecimiento
Con retención controlada, ahora sí invertir en adquisición.
Entregables
1. Programa de referidos dentro del producto
- "Invitá a otro negocio y ambos reciben 1 mes con 50% de descuento"
- Compartir por WhatsApp (obvio — es donde viven sus clientes)
- Tracking automático de referidos
2. Integraciones con herramientas que ya usan las PyMEs
- Google Calendar (para clínicas y profesionales que agendan citas)
- Google Sheets (como CRM básico — exportar contactos y conversaciones)
- Estas dos integraciones cubren el 80% de los pedidos que van a recibir
3. Widget de "Powered by [tu producto]" en las conversaciones
- Sutil pero presente: cada conversación del agente es marketing gratuito
- Link a landing con contexto: "¿Querés un agente así para tu negocio?"
4. Contenido y comunidad
- Webinar mensual: "Cómo [tipo de negocio] automatizó su soporte por WhatsApp"
- Casos de éxito escritos (con métricas reales de clientes que den permiso)
- Grupo de WhatsApp de clientes (comunidad = retención + feedback directo)
Justificación
El canal de adquisición más barato para PyMEs en LATAM es el boca a boca entre colegas del mismo rubro. Un contador le dice a otro contador. El programa de referidos y el widget formalizan esto. Las integraciones con Calendar y Sheets no son features — son razones para que el cliente use la plataforma todos los días en lugar de solo "tenerla andando".
Asignación de equipo
- Dev 1: programa de referidos + widget
- Dev 2: integración Google Calendar + Google Sheets
- UX (20hs): flujo de referidos, UX de las integraciones
Founders (50%+ en ventas)
- Outbound en nuevas verticales: inmobiliarias, gimnasios, restaurants
- Partnerships con asociaciones profesionales (Colegio de Contadores, etc.)
- Hablar con cada cliente que cancela para entender por qué
Métrica de éxito
- 35-40 clientes pagos
-
20% de nuevos clientes vienen por referidos
- Churn < 12%
MES 6: Escalar a 50 y preparar el siguiente salto
Entregables
1. Onboarding con AI (meta, pero funciona)
- Un agente de AI que ayuda al cliente a configurar su propio agente
- Hace preguntas sobre el negocio, sugiere respuestas, configura todo
- Esto baja el onboarding de 30 min a 10-15 min y elimina la necesidad de soporte humano en setup
2. Multi-idioma (español + portugués)
- Brasil es 3x el mercado de Argentina
- No requiere rebuild — es configuración de prompts + traducción de UI
- Esto abre la puerta a un mercado masivo para el siguiente período
3. Analytics avanzados
- Reportes mensuales automáticos por email: "Tu agente atendió X conversaciones, resolvió Y%, te ahorró Z horas"
- Benchmark vs. otros negocios del mismo rubro (anonimizado)
- Esto es la herramienta de retención definitiva: el cliente ve el ROI sin buscarlo
4. API pública (básica)
- Para los clientes más técnicos o agencias que quieran integrar
- Esto abre un canal de distribución nuevo: agencias digitales que revenden
Justificación
El onboarding con AI es el multiplicador que permite escalar sin agregar soporte humano. Portugués abre Brasil, que es donde está el crecimiento real en LATAM. Los analytics automáticos son lo que convierte un "nice to have" en un "no puedo cancelar esto".
Asignación de equipo
- Dev 1: onboarding AI + API básica
- Dev 2: multi-idioma + analytics avanzados
- UX (20hs): reportes automáticos, adaptación UI para portugués
Métrica de éxito
- 50 clientes pagos → MRR $7,450
- Churn mensual < 10%
- Onboarding self-service < 15
Criterios evaluados
Prioriza correctamente onboarding en mes 1 y calidad/seguridad del agente (memoria, ambigüedad, handoff) en mes 2, alineado con churn 40% y feedback de beta. Aun así, mete varias apuestas de crecimiento/expansión (multi-idioma, API, integraciones) antes de demostrar churn <10% y onboarding estable en cohortes más grandes.
El roadmap abre con un diagnóstico explícito del problema ('el problema más urgente no es el AI agent, es que el 40% se va porque no puede ni configurarlo') y dedica los meses 1 y 2 exclusivamente a resolver onboarding y calidad de respuesta antes de cualquier feature de crecimiento. Reconoce explícitamente que crecer sin retención es 'llenar un balde con agujeros'. Solo no llega a 100 porque podría haber cuantificado mejor el impacto matemático del churn sobre la meta de 50 clientes.
Está bien calibrado en general: usa soluciones simples (resúmenes LLM en vez de framework propio, Stripe/MercadoPago, Google Sheets como CRM). Sin embargo, la carga de trabajo por mes es ambiciosa para 2 devs — el mes 3 tiene billing + dashboard + landing + trial, y el mes 6 incluye onboarding AI + API + multi-idioma + analytics, lo cual parece excesivo. La meta de 15-20 clientes pagos al final del mes 3 partiendo de 5 beta con 40% churn también es optimista. El budget de $50k incluyendo sueldos para 2.5 personas durante 6 meses es extremadamente ajustado y el roadmap no aborda esto directamente.
Meses 1-3 son relativamente realizables, pero el plan acumula demasiados frentes para 2 devs (memoria+resúmenes, billing, dashboard, training loop, integraciones, multi-idioma, analytics avanzados, API) dentro de 6 meses y $50k. Además, asumir 15-20 clientes pagos al mes 3 y 50 al mes 6 con founders al 50% en ventas puede ser optimista sin un motor de adquisición más concreto y con churn aún en corrección.
Cubre producto y GTM: billing, trial, landing, playbook, outbound, referidos, partnerships, comunidad y métricas de éxito por mes. Podría fortalecer customer success operacional (soporte, onboarding asistido temporal, health scoring) y el plan de adquisición cuantificado para llegar a 50 con conversión/ciclo de ventas.
Cubre producto (onboarding, memoria, integraciones), go-to-market (landing, referidos, outbound, partnerships con asociaciones profesionales), pricing ($149 con justificación de willingness-to-pay), billing, contenido/comunidad, y asigna responsabilidades a founders para ventas. Incluye métricas de éxito por mes. Cubre customer success con alertas proactivas y reportes de ROI. Solo le falta algo más de detalle en unit economics y en qué pasa si el runway se acorta.
Cada mes tiene justificación explícita conectada a datos del brief (churn 40%, feedback de ambigüedad, budget limitado, competencia mejor financiada). Destaca la decisión técnica de no construir framework propio de memoria, la justificación del pricing de $149, y el razonamiento de switching costs con entrenamiento continuo. Menciona trade-offs como no competir en features vs Treble.ai y no hacer freemium, aunque podría profundizar más en qué se sacrifica en cada mes.
Da racionales claros conectados a los datos (setup difícil, ambigüedad/contexto, necesidad de safety net humano, ROI para justificar precio). Sin embargo, falta explicitar trade-offs duros (qué se posterga/elimina) y hay afirmaciones discutibles/no fundamentadas (costos de tokens, “Stripe soporta LATAM”, impacto del pricing) sin evidencia.
Writing
6 pruebas
W1.1emails87.9
Respuesta del modelo
Asunto: Reducir costos de soporte sin perder calidad — caso para [Empresa]
Hola [Nombre],
Soy [Tu nombre], de Ikilabs. Ayudamos a empresas de e-commerce en LATAM a escalar su soporte sin seguir sumando headcount.
Vi que [Empresa] está creciendo fuerte. En nuestra experiencia, equipos de soporte de ese tamaño suelen dedicar más del 60% de su tiempo a responder siempre lo mismo: estado de pedidos, devoluciones, preguntas de FAQ.
Lo que hacemos concretamente:
- Agentes AI que resuelven esos tickets solos, conectados a tu plataforma de e-commerce y CRM para dar respuestas con datos reales del cliente (no respuestas genéricas).
- Automatización del enrutamiento: lo que el agente no resuelve llega clasificado y con un borrador de respuesta para que tu equipo solo apruebe.
El resultado típico: el equipo de soporte pasa de apagar incendios a ocuparse de los casos que realmente necesitan a una persona. Implementamos end-to-end e incluimos soporte post-launch.
¿Tiene sentido una llamada de 20 minutos esta semana para que me cuentes cómo opera el equipo hoy? Sin compromiso, y si no hay fit lo digo rápido.
Un saludo, [Tu nombre] Ikilabs — Consultoría AI para empresas B2B [email] · [teléfono]
Criterios evaluados
Evita buzzwords prohibidas y mantiene un lenguaje bastante natural y concreto. Aun así, algunas construcciones típicas de outreach (“en nuestra experiencia”, “sin compromiso, y si no hay fit lo digo rápido”) suenan plantilla.
Evita completamente las frases prohibidas. El lenguaje es natural y específico: 'apagar incendios', 'si no hay fit lo digo rápido', 'sin seguir sumando headcount'. Suena como un email real de un consultor experimentado. Solo alguna estructura como los bullet points con negritas podría delatarlo levemente, pero es un formato legítimo en cold emails.
Ataca claramente el dolor de soporte repetitivo y caro, menciona casos concretos (estado de pedidos, devoluciones) y automatización con drafts. Le falta anclar mejor al dato del contexto (8 agentes) y usar la cifra prometida (60-70% de tickets) para ser más específico.
Conecta muy bien con el pain point: menciona explícitamente estado de pedidos, devoluciones, FAQ, y el dato del 60% de tiempo en preguntas repetitivas. Presenta los dos servicios más relevantes (agentes AI y automatización de enrutamiento). Podría haber sido más específico mencionando el equipo de 8 personas o dando una cifra concreta de ahorro potencial (ej: 'reducir de 8 a 3 personas'), lo cual habría elevado el score.
Tono B2B correcto: directo, orientado a operaciones y con foco en headcount/costos, sin sonar demasiado vendedor. El “vi que [Empresa] está creciendo fuerte” puede sentirse un poco asumido si no hay evidencia.
El tono es directo, concreto y respetuoso — ideal para un CTO técnico. Frases como 'si no hay fit lo digo rápido' y 'sin compromiso' transmiten confianza sin ser agresivas. Quizás 'Vi que [Empresa] está creciendo fuerte' suena ligeramente presuntuoso sin evidencia concreta, pero en general está muy bien calibrado.
Redacción clara, sin errores ortográficos evidentes y con buena puntuación. Solo detalles menores de estilo (mezcla de inglés como “headcount” y construcciones algo coloquiales).
Gramática y ortografía correctas en todo el email. El uso de 'compromiso' en lugar de 'compromiso' es correcto, la puntuación es adecuada. Sin errores detectables.
Incluye asunto, cuerpo, firma y una CTA concreta para una llamada de 20 minutos. Está dentro del límite de 200 palabras.
Tiene asunto claro y atractivo, cuerpo bien estructurado con bullets, firma profesional con datos de contacto. El CTA es muy concreto: llamada de 20 minutos esta semana. Conté aproximadamente 185 palabras, dentro del límite. Excelente estructura.
W5.1marketing86.4
Respuesta del modelo
Opciones de Hero Section para Ikilabs
Opción A — Enfoque en velocidad y resultado tangible
Headline: AI que funciona en tus sistemas reales
Subheadline: Integramos agentes inteligentes y automatización en tu operación actual — sin reescribir lo que ya tenés. En 2 semanas ves resultados con tus propios datos, no con slides.
CTA: Agendá el discovery →
Opción B — Enfoque en el dolor del fracaso previo
Headline: Menos presentaciones. Más código en producción.
Subheadline: Diseñamos e integramos agentes de AI para soporte, operaciones y procesos que hoy escalan a fuerza de personas. Prototipo funcional con datos reales en 2 semanas.
CTA: Hablemos de tu caso →
Opción C — Enfoque en el problema de escala
Headline: Tus procesos manuales no van a escalar solos
Subheadline: Construimos agentes de AI y automatizaciones que se integran a tus sistemas existentes. Arrancamos con un discovery de 1 semana y en 2 semanas tenés un prototipo corriendo con tus datos — antes de comprometer más presupuesto.
CTA: Empezá con un discovery →
Opción D — Enfoque en el diferenciador técnico
Headline: Integramos AI donde tu operación la necesita
Subheadline: No reemplazamos tu stack: conectamos LLMs y agentes inteligentes a lo que ya funciona. Prototipo con datos reales en 2 semanas. Si no ves ROI, no seguimos.
CTA: Agendá una llamada técnica →
Notas sobre las opciones
- Opción A es la más equilibrada y funciona bien como default.
- Opción B es la más agresiva — ideal si el público objetivo son CTOs quemados por experiencias previas con otras consultoras.
- Opción C lidera con el problema, buena para SEO y para visitantes que todavía están diagnosticando su situación.
- Opción D habla más directo al perfil técnico y pone el diferenciador de integración al frente.
Las cuatro se pueden A/B testear. Si querés, te armo variantes combinando elementos entre ellas o adapto el tono para un perfil de decisor específico (CTO vs. COO vs. CEO).
Criterios evaluados
Todas las opciones incluyen headline, subheadline y CTA. Headline y subheadline cumplen el largo; el CTA a veces queda en 2 palabras (“Hablemos de tu caso”) si se ignora el símbolo →, aunque en general está dentro del rango.
Las 4 opciones incluyen los 3 elementos requeridos. Todos los headlines están dentro de las 8 palabras, los subheadlines son de 1-2 oraciones (la opción C tiene 2 oraciones largas que podrían considerarse 3 cláusulas pero técnicamente son 2), y los CTAs están en el rango de 3-5 palabras con orientación a acción concreta. Cumplimiento sólido.
Evita los buzzwords prohibidos y no cae en promesas tipo “siguiente nivel”. Pero usa “AI” de forma algo genérica en varios headlines (“AI que funciona…”, “Integramos AI…”) y aparecen formulaciones algo marketineras (“agentes inteligentes”) sin anclar siempre a un problema concreto.
No aparece ninguno de los buzzwords prohibidos ni frases de promesa vacía. Cada mención de AI está anclada a un contexto concreto: 'AI para soporte, operaciones y procesos', 'AI donde tu operación la necesita', 'agentes de AI'. El tono es directo, técnico y accesible. La frase 'agentes inteligentes' en opción A y D podría rozar lo genérico pero no viola ninguna restricción explícita.
Varias líneas son memorables (“Menos presentaciones. Más código en producción.”) y específicas (“con tus propios datos, no con slides”). Aun así, la estructura en “opciones”, el lenguaje repetido (“en 2 semanas…”) y algunos giros estándar de SaaS/consultoría lo hacen sentir parcialmente plantilla.
El formato de '4 opciones con notas explicativas' es un patrón muy típico de respuesta de LLM. Frases como 'AI que funciona en tus sistemas reales' y 'donde tu operación la necesita' son competentes pero no especialmente memorables. La opción B ('Menos presentaciones. Más código en producción.') es la más distintiva y con más personalidad. El uso de voseo argentino ('tenés', 'agendá') le da autenticidad regional, pero el conjunto se siente más como un buen framework que como copy final pulido por un copywriter senior.
Habla a dolores reales (integrar en sistemas existentes, procesos manuales que no escalan, hartazgo de consultoras con slides) y destaca el diferenciador de discovery + prototipo con datos reales en 2 semanas. Podría reforzar más el enfoque LATAM/industrias o ejemplos de procesos para ser aún más específico.
Cada opción ataca un dolor real distinto: fracaso con otras consultoras (B), procesos que no escalan (C), integración sin reescribir (A y D). El diferenciador de prototipo en 2 semanas con datos reales aparece consistentemente. El ROI tangible se menciona explícitamente en la D. Muy bien alineado con el briefing, cubre los tres dolores principales del cliente objetivo.
W2.1proposals88.6
Respuesta del modelo
El bug que no fallaba (hasta que fallaba)
Estuvimos tres semanas depurando un bug de producción que perdía órdenes aleatoriamente. Tres semanas. El problema resultó ser un race condition en el procesamiento paralelo de webhooks de Stripe: cuando dos webhooks llegaban al mismo tiempo para el mismo pedido, ambos hilos procesaban la orden y creaban duplicados o, mejor aún, se pisaban entre sí. Clásico.
La solución fue casi insultantemente simple: un lock distribuido con Redis, un SET NX con TTL de 30 segundos. Lo que más tiempo nos llevó no fue arreglar el bug sino reproducirlo en local, porque por supuesto en tu máquina todo procesa secuencialmente y el mundo es un lugar ordenado y justo.
Ahora tenemos una regla no escrita: cada vez que algo "falla aleatoriamente", lo primero que revisamos es si puede ser concurrencia. Casi siempre lo es.
Criterios evaluados
Incluye todas las ideas clave: 3 semanas, pérdida aleatoria de órdenes, race condition con webhooks de Stripe, doble procesamiento para el mismo pedido, lock distribuido en Redis con SET NX y TTL 30s, dificultad de reproducir en local y la lección sobre concurrencia. Agrega detalles no provistos (“hilos”, “tu máquina todo procesa secuencialmente”, “Casi siempre lo es”), que aunque plausibles, son ideas nuevas.
Todas las ideas originales están presentes: las 3 semanas, el bug de producción, el race condition, webhooks de Stripe, duplicados/pisadas, Redis SET NX con TTL de 30 segundos, la dificultad de reproducir en local, y la lección aprendida. La frase 'Casi siempre lo es' al final es una pequeña adición que no estaba en las notas originales, y 'ambos hilos' añade el detalle de 'hilos' que no estaba explícito. Son adiciones menores pero existentes.
La voz es de dev senior, directa, en primera persona plural y con humor seco; no hay marketing y es prosa. Se pasa del límite de 150 palabras y el título en formato Markdown no estaba pedido (aunque no es bullet list).
Primera persona plural consistente, prosa sin bullet points, tono directo de dev senior con humor seco bien logrado ('Clásico', 'casi insultantemente simple', 'el mundo es un lugar ordenado y justo'). Sin frases motivacionales ni marketing. El conteo de palabras es ~140, dentro del límite de 150.
Suena muy auténtico y humano. El humor seco, las frases cortas como 'Tres semanas.' y 'Clásico.' dan naturalidad. El título con paréntesis es un recurso que un dev real usaría. Solo la frase 'el mundo es un lugar ordenado y justo' podría sonar ligeramente elaborada, pero encaja bien con el tono irónico.
Suena bastante humano por el ritmo, repeticiones (“Tres semanas.”) y la ironía técnica. Aun así, algunas frases se sienten “redondeadas” y ligeramente formulaicas (p.ej., el remate “Casi siempre lo es”).
Gramática y ortografía correctas, con puntuación natural y uso adecuado de términos técnicos. No se observan errores relevantes.
Gramática y ortografía impecables. Uso correcto de signos, formato de código inline para SET NX, puntuación adecuada. Sin errores detectables.
W6.1translation96.0
Respuesta del modelo
When two requests come in at the same time and both try to modify the same record, you've got a concurrency problem. The most common solution is optimistic locking: instead of locking the record while you modify it, you store a version of the current state and at write time you check that nobody else modified it in the meantime.
In TypeORM, this is implemented with a version field on the entity. The UPDATE doesn't just filter by id — it also filters by version = :currentVersion. If the result affects 0 rows, someone else won the race — you throw an error and the client retries.
What tutorials usually don't explain is when not to use optimistic locking: when contention is high (lots of users modifying the same record in quick succession), you'll end up with a cascade of retries that can be worse than a pessimistic lock.
Criterios evaluados
Suena natural y profesional, con construcciones idiomáticas (“you've got a concurrency problem”, “won the race”, “cascade of retries”). Mantiene el tono directo y pedagógico sin calcar estructuras del español.
The text reads like it was originally written in English by a senior developer. Excellent adaptations like 'you've got a concurrency problem', 'someone else won the race', 'in quick succession' (instead of literal 'seguido'), and 'you'll end up with' all sound completely natural. The use of *not* in italics for emphasis is a nice touch that mirrors the pedagogical tone.
The translation preserves the full meaning of the original with high fidelity. All technical concepts are intact, the TypeORM example is preserved exactly, and no information is omitted. The only very minor point is 'you store a version of the current state' which is a faithful but slightly ambiguous rendering of 'guardás una versión del estado actual' — it could be read as storing a copy of the state rather than a version number, but in context it's clear enough.
Preserva el significado completo del original, incluyendo la explicación del optimistic locking y el caso donde no conviene usarlo. El ejemplo de TypeORM (version field y el filtro por version = :currentVersion) se mantiene intacto; solo hay ajustes menores de estilo sin pérdida de contenido.
Gramática y ortografía impecables; puntuación clara y consistente. No se observan errores.
The English is grammatically flawless throughout. Sentence structure, punctuation, and spelling are all correct. No errors detected.
La terminología técnica es correcta y consistente: optimistic locking, version field, UPDATE, contention, retries, pessimistic lock. El fragmento técnico se conserva exactamente como corresponde.
All technical terms are correctly handled: optimistic locking preserved as-is, 'lock pesimista' correctly rendered as 'pessimistic lock', 'contención' as 'contention', 'reintentos' as 'retries', version field, UPDATE query syntax all preserved. Perfect terminology choices throughout.
W3.1proposals91.0
Respuesta del modelo
Nuestra metodología
Semana 1 — Discovery. Arrancamos con entrevistas a tu equipo y un mapeo concreto de los procesos actuales. Separamos lo que se puede resolver rápido de lo que necesita un abordaje más largo. Al final de la semana tenés un diagnóstico claro y un plan con prioridades.
Semanas 2 y 3 — Prototipo funcional. Construimos algo que funciona con datos reales de tu operación, no slides ni mockups. El objetivo es que puedas medir impacto antes de comprometer más presupuesto.
Semanas 4 y 5 — Iteración. Hacemos uno o dos ciclos de feedback con tu equipo. Ajustamos, corregimos, mejoramos. Ves el producto en cada etapa y validás que resuelve lo que tiene que resolver antes de ir a producción.
Post-lanzamiento — Soporte incluido. Todos nuestros proyectos incluyen 30 días de soporte sin costo adicional. Si algo se rompe o necesita ajuste, lo resolvemos.
Cada etapa tiene un entregable definido. Sabés qué estamos haciendo, cuánto falta y qué viene después.
Criterios evaluados
Cubre los 4 puntos en el orden pedido: discovery 1 semana, prototipo 2 semanas con datos reales, 1-2 ciclos de iteración antes de producción y soporte post-lanzamiento de 30 días. La única desviación es que agrega “Semanas 4 y 5” (no solicitado) y no explicita tan claramente el “antes de ir a producción” como hito, aunque se entiende.
Cubre los 4 puntos en el orden correcto: discovery de 1 semana con entrevistas, mapeo y quick wins vs. largo plazo; prototipo funcional con datos reales y mención de ROI/impacto antes de más presupuesto; iteración con 1-2 ciclos de feedback antes de producción; soporte post-launch de 30 días incluido. Solo se podría señalar que 'identificación de quick wins vs. proyectos de largo plazo' está parafraseado como 'separamos lo que se puede resolver rápido de lo que necesita un abordaje más largo', lo cual es correcto pero ligeramente menos explícito. La mención de ROI tangible se traduce como 'medir impacto', que es fiel al espíritu.
Usa primera persona plural y evita jerga de startup y frases vacías; es claro y orientado a resultados (ROI/impacto medible, entregables). Baja un poco por el uso de “tu”/“tenés” (más informal de lo institucional) y por algún énfasis algo genérico.
Voz impecable: primera persona plural consistente, cero jerga de startup, cero frases vacías, tono directo y experto sin ser corporativo. El tuteo ('tenés', 'validás') refuerza lo accesible. La línea final sobre transparencia ('Sabés qué estamos haciendo, cuánto falta y qué viene después') es excelente. No hay ni un solo desliz.
Está dentro del máximo de 180 palabras y no presenta errores gramaticales u ortográficos relevantes. La puntuación y el registro son consistentes.
Conteo aproximado: ~155 palabras, dentro del límite de 180. Gramática correcta, uso consistente del voseo rioplatense. Sin errores ortográficos detectados.
Es directo, específico y con estructura clara, sin muletillas tipo “en este sentido”. Aun así, la cadencia y el formato por etapas con frases cortas (“Ajustamos, corregimos, mejoramos.”) se sienten algo plantilla.
Suena muy humano y con criterio propio. Frases como 'no slides ni mockups', 'si algo se rompe o necesita ajuste, lo resolvemos' y 'validás que resuelve lo que tiene que resolver' tienen un tono natural y directo que no es típico de LLMs. La estructura con semanas numeradas es funcional sin ser formulaica.
W4.1social-media86.2
Respuesta del modelo
La mayoría de las empresas que contratan "AI consultants" terminan con un chatbot encima de ChatGPT y le dicen transformación digital.
El problema no es la tecnología. Es que nadie en la empresa tiene claro qué problema están resolviendo realmente.
Nosotros siempre arrancamos con la misma pregunta: "¿Qué hace tu equipo hoy que no debería hacer un humano?" Esa pregunta sola filtra el 80% de los proyectos que no tienen sentido.
Lo que sí funciona es automatizar tareas repetitivas concretas. No "mejorar la experiencia del cliente" en abstracto. Concreto: esta persona hace esto 40 veces por día, no necesita criterio humano, se puede automatizar. Eso.
Y los proyectos de AI que fallan casi nunca fallan por el modelo. Fallan porque los datos son un desastre. No hay estructura, no hay limpieza, no hay pipeline. Se invierte en el algoritmo y se ignora todo lo que lo alimenta.
Llevamos años viendo el mismo patrón. Las empresas que sacan valor real de AI son las que primero hicieron el trabajo aburrido que nadie quiere hacer.
Criterios evaluados
Incluye todos los puntos centrales (chatbot ≠ transformación, problema mal definido, pregunta filtro, foco en tareas concretas, fallas por datos sucios). Pero agrega ideas no presentes: “pipeline”, “invertir en el algoritmo”, “años viendo el patrón” y “el trabajo aburrido”, que amplían la posición y violan el “no expandas / no agregues ideas tuyas”.
Todos los cinco puntos están presentes. Sin embargo, se agregan algunas ideas que no estaban en el original: 'esta persona hace esto 40 veces por día, no necesita criterio humano' es un ejemplo inventado; 'No hay estructura, no hay pipeline' expande más allá de lo pedido; y la frase final sobre 'el trabajo aburrido que nadie quiere hacer' es una idea nueva no presente en la opinión cruda. Son expansiones menores pero van contra la instrucción explícita de no expandir ni agregar ideas.
Suena a fundador técnico y directo, sin clichés típicos de LinkedIn, sin emojis ni exclamaciones, y termina con una observación. Está dentro del límite de 180 palabras (aprox. 150-165).
Excelente cumplimiento del estilo pedido: sin clichés de LinkedIn, sin emojis, sin exclamaciones, sin preguntas retóricas al final, termina con una observación. Suena a fundador técnico con experiencia real. El conteo de palabras está alrededor de 160, dentro del límite. Primera persona, directo, con criterio propio. Tono auténtico y sin engagement bait.
Gramática y ortografía sólidas; puntuación correcta y comillas bien usadas. Solo detalles de estilo (repetición y cortes muy telegráficos) más que errores.
Gramática y ortografía correctas en español. El uso de fragmentos y oraciones cortas es intencional y estilístico, no errores gramaticales. Sin faltas detectables.
El texto fluye bastante natural y con criterio, con algunas construcciones algo formulaicas (“Llevamos años viendo el mismo patrón”) que pueden sonar a plantilla, pero en general se siente humano.
Suena bastante natural y humano. El ritmo de frases cortas y la estructura fragmentada ('Eso.') le dan autenticidad. Algunas construcciones como la repetición estructurada 'No hay estructura, no hay limpieza, no hay pipeline' tienen un leve aire a texto pulido por AI, pero en general fluye bien como escritura de una persona real.