Saltar al contenido principal

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) │
└──────────────────────────────────────────────────────────────────┘
info

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_intakepersonalityplanactive)
  • 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

TablaPropósito
channel_usersRegistros de usuario del lado del canal con estado de onboarding
otp_codesAlmacenamiento de códigos OTP (hasheados con bcrypt, con seguimiento de TTL)
chat_messagesHistorial de conversación persistente por usuario
transactionsRegistros de transacciones del usuario
user_goalsMetas de ahorro del usuario
money_move_tasksPassos programados y completados
spending_alertsRegistros de alertas de gasto excesivo en sobres

Tablas de la Intelligence API

TablaPropósito
api_clientsCredenciales de clientes de API
api_rate_windowsEstado del límite de tasa
usersRegistros de identidad de usuarios
financial_intake_resultsInstantáneas de intake enviadas
financial_plansRegistros de planes generados
personality_questionsCatálogo de preguntas del quiz
money_moves_catalogBiblioteca 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ódigoHTTPDescripción
INTAKE_REQUIRED422El intake financiero debe enviarse antes de generar un plan
PERSONALITY_REQUIRED422Debe proporcionarse personality_type (si es requerido por el endpoint)
PLAN_REQUIRED422El plan debe generarse antes de que este endpoint pueda ser llamado
INVALID_CODE400El código OTP es incorrecto o ya fue utilizado
CODE_EXPIRED400El código OTP superó su TTL de 10 minutos