Arquitectura
La plataforma Ozzie está dividida en dos capas: una Channel API stateful que las aplicaciones de consumo llaman directamente, y una Intelligence API sin estado que el canal hace proxy internamente.
Diseño de dos capas
┌──────────────────────────────────────────────────────────────────┐
│ YOUR APPLICATION (Lovable / consumer app) │
└─────────────────────────┬────────────────────────────────────────┘
│ all API calls
▼
┌──────────────────────────────── ──────────────────────────────────┐
│ Layer 1 — Channel API (channel.ozzieapp.com) │
│ │
│ • Stateful: owns users, OTP, onboarding, chat history │
│ • Handles end-user authentication (OTP via WhatsApp or email) │
│ • Stores channel_users, otp_codes, chat_messages, transactions │
│ • Proxies intelligence requests to Layer 2 internally │
│ │
│ Base URL: https://channel.ozzieapp.com/v1 │
└─────────────────────────┬────────────────────────────────────────┘
│ internal proxy (not called by consumers)
▼
┌──────────────────────────────────────────────────────────────────┐
│ Layer 2 — Intelligence API (api.ozzieapp.com) │
│ │
│ • Stateless: receives full context as request input │
│ • Returns plans, envelopes, chat responses, quiz scores │
│ • No session affinity, no stored history │
│ • personality_type is always a caller-supplied parameter │
│ │
│ Base URL: https://api.ozzieapp.com/v1 (internal use only) │
└──────────────────────────────────────────────────────────────────┘
Las aplicaciones de consumo — incluida Lovable — llaman únicamente a channel.ozzieapp.com. La capa de inteligencia en api.ozzieapp.com es un servicio interno; no la llamas directamente.
Por qué dos capas
La Channel API maneja todo lo stateful:
- Identidad del usuario final y autenticación OTP
- Progresión de etapas de onboarding (
financial_intake→personality→plan→active) - Historial de chat persistente
- Transacciones, metas y alertas de gasto
La Intelligence API maneja todo lo que es solo cómputo:
- Inferencia de chat IA sin estado
- Puntuación de personalidad
- Generación de planes y sobres
- Recomendaciones de movimientos de dinero
Mantenerlas separadas permite que la capa de inteligencia escale de forma independiente y sea reutilizable en diferentes canales sin nunca poseer el estado del usuario.
Cómo fluye personality_type
La personalidad se puntúa una sola vez (usando los endpoints del quiz) y es almacenada por tu aplicación. En cada llamada de inteligencia posterior, tu aplicación lee el personality_type almacenado y lo pasa como parámetro:
User takes quiz
│
▼
POST /v1/personality/score
→ returns personality_type (e.g. "GIVER")
→ your app stores it
│
▼
On every subsequent intelligence call, your app passes it explicitly:
GET /v1/users/:id/financial-profile?personality_type=GIVER
POST /v1/users/:id/plan/generate body: { personality_type: "GIVER" }
POST /v1/money-moves/generate body: { personality_type: "GIVER", ... }
Principio clave: personality_type nunca se almacena en la capa de inteligencia. Tu aplicación es la fuente y siempre lo pasa explícitamente.
Tablas de la base de datos
Tablas de la Channel API
| Tabla | Propósito |
|---|---|
channel_users | Registros de usuario del lado del canal con estado de onboarding |
otp_codes | Almacenamiento de códigos OTP (hasheados con bcrypt, con seguimiento de TTL) |
chat_messages | Historial de conversación persistente por usuario |
transactions | Registros de transacciones del usuario |
user_goals | Metas de ahorro del usuario |
money_move_tasks | Passos programados y completados |
spending_alerts | Registros de alertas de gasto excesivo en sobres |
Tablas de la Intelligence API
| Tabla | Propósito |
|---|---|
api_clients | Credenciales de clientes de API |
api_rate_windows | Estado del límite de tasa |
users | Registros de identidad de usuarios |
financial_intake_results | Instantáneas de intake enviadas |
financial_plans | Registros de planes generados |
personality_questions | Catálogo de preguntas del quiz |
money_moves_catalog | Biblioteca de plantillas de movimientos |
Ejemplos de flujo de solicitudes
Autenticación OTP (capa del canal)
Consumer app: POST /v1/auth/otp/request
body: { identifier: "+5511999990000", language: "pt" }
│
▼
channel-api:
1. Generates 6-digit code, bcrypt-hashes it, stores with 10-min TTL
2. Sends code via WhatsApp (auto-detected from E.164 phone)
3. Returns { channel, masked, expires_at }
Consumer app: POST /v1/auth/otp/verify
body: { identifier: "+5511999990000", code: "482910" }
│
▼
channel-api:
1. Validates code against bcrypt hash
2. Returns { user_id, core_user_id, onboarding_stage }
Generación de plan (enrutado a través de la capa del canal)
Consumer app: POST /v1/users/usr_123/plan/generate
body: { personality_type: "STOCKPILER" }
│
▼
channel-api:
1. Verifies user belongs to this client
2. Loads user's stored intake data
3. Proxies to intelligence layer
│
▼
intelligence-api:
1. Applies STOCKPILER envelope weights
2. Generates plan with action items
3. Returns plan
│
▼
channel-api:
4. Persists plan to channel DB
5. Returns plan to consumer app
Completado de chat (con historial persistente)
Consumer app: POST /v1/users/usr_123/chat/completion
body: {
message: "Am I on track?",
personality_type: "STOCKPILER",
user_context: { ... }
}
│
▼
channel-api:
1. Loads conversation history from chat_messages
2. Proxies to intelligence layer with history injected
│
▼
intelligence-api:
1. Uses message + history + personality_type + user_context
2. Returns AI reply
│
▼
channel-api:
3. Persists user message + assistant reply to chat_messages
4. Returns reply to consumer app
Inyección de contexto
El endpoint de chat acepta un objeto user_context que tu aplicación ensambla desde su propia base de datos antes de cada llamada. Este es el mecanismo principal para pasar el estado rico y en tiempo real del usuario a la capa de inteligencia sin que la API necesite almacenar nada de eso.
Campos que tu aplicación puede inyectar:
- Identidad y preferencias:
name,language,personality_type,plan_tier,plan_speed - Instantánea financiera:
monthly_income,goal_progress,total_saved_cents - Historial de comportamiento:
completed_moves_count,last_confirmation_note,next_move - Señales de sesión:
entry_point,spending_alert_envelope,spending_alert_ratio - Contexto de recuperación:
rag_context— texto libre de tu propio pipeline de recuperación
Todos los campos de user_context son opcionales. La capa de inteligencia usa lo que esté presente para personalizar la respuesta; los campos faltantes simplemente no se referencian.
Your database channel-api request
──────────────── ──────────────────────────────
users.name ───► user_context.name
users.personality_type ───► user_context.personality_type
plans.plan_tier ───► user_context.plan_tier
plans.plan_speed ───► user_context.plan_speed
goals.percentage ───► user_context.goal_progress.percentage
moves.completed_count ───► user_context.completed_moves_count
moves.next_date ───► user_context.next_move.date
session.entry_point ───► user_context.entry_point
rag_pipeline.output ───► user_context.rag_context
Enrutamiento por entry-point
Cuando un usuario abre la pantalla de chat, tu aplicación sabe cómo llegó. Pasar entry_point en user_context permite a Ozzie generar un mensaje de apertura apropiado al contexto (entry_point_opener) que tu aplicación puede mostrar antes de que el usuario escriba algo.
User arrives entry_point value entry_point_opener
──────────────── ───────────────── ──────────────────────────────
Opens app directly ───► "unprompted" ───► Personalized greeting with passos + next move
WhatsApp expense prompt ───► "weekly_expense" ───► Asks for expenses with input options
Overspending notification ───► "spending_alert" ───► References envelope + asks about recalibration
Monthly review link ───► "monthly_review" ───► Summarizes last month + asks about this month
Passo reminder ───► "move_reminder" ───► null (channel renders the passo card instead)
No entry_point sent ───► (omit field) ───► null
Tu aplicación debe resolver entry_point desde los parámetros de lanzamiento de la sesión (deep link, payload de notificación, etc.) antes de ensamblar el objeto user_context.
Estados de error
| Código | HTTP | Descripción |
|---|---|---|
INTAKE_REQUIRED | 422 | El intake financiero debe enviarse antes de generar un plan |
PERSONALITY_REQUIRED | 422 | Debe proporcionarse personality_type (si es requerido por el endpoint) |
PLAN_REQUIRED | 422 | El plan debe generarse antes de que este endpoint pueda ser llamado |
INVALID_CODE | 400 | El código OTP es incorrecto o ya fue utilizado |
CODE_EXPIRED | 400 | El código OTP superó su TTL de 10 minutos |