Arquitetura
A plataforma Ozzie é dividida em duas camadas: uma Channel API stateful que os aplicativos para consumidores chamam diretamente, e uma Intelligence API sem estado que o canal faz proxy internamente.
Design em duas camadas
┌──────────────────────────────────────────────────────────────────┐
│ 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) │
└──────────────────────────────────────────────────────────────────┘
Os aplicativos para consumidores — incluindo o Lovable — chamam apenas channel.ozzieapp.com. A camada de inteligência em api.ozzieapp.com é um serviço interno; você não a chama diretamente.
Por que duas camadas?
A Channel API gerencia tudo que é stateful:
- Identidade do usuário final e autenticação OTP
- Progressão do estágio de onboarding (
financial_intake→personality→plan→active) - Histórico de chat persistente
- Transações, metas e alertas de gastos
A Intelligence API gerencia tudo que é computacional:
- Inferência de chat com IA sem estado
- Pontuação de personalidade
- Geração de plano e envelopes
- Recomendações de movimentos financeiros
Mantê-las separadas permite que a camada de inteligência escale de forma independente e seja reutilizada em diferentes canais sem nunca ser responsável pelo estado do usuário.
Como personality_type flui
A personalidade é pontuada uma vez (usando os endpoints do quiz) e armazenada pela sua aplicação. Em cada chamada de inteligência subsequente, sua aplicação lê o personality_type armazenado e o passa 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", ... }
Princípio fundamental: personality_type nunca é armazenado na camada de inteligência. Sua aplicação é a fonte e sempre o passa explicitamente.
Tabelas do banco de dados
Tabelas da Channel API
| Tabela | Finalidade |
|---|---|
channel_users | Registros de usuários do canal com estado de onboarding |
otp_codes | Armazenamento de códigos OTP (hash bcrypt, com controle de TTL) |
chat_messages | Histórico de conversa persistente por usuário |
transactions | Registros de transações do usuário |
user_goals | Metas de poupança do usuário |
money_move_tasks | Passos agendados e concluídos |
spending_alerts | Registros de alertas de gastos excessivos por envelope |
Tabelas da Intelligence API
| Tabela | Finalidade |
|---|---|
api_clients | Credenciais de clientes de API |
api_rate_windows | Estado de rate limiting |
users | Registros de identidade de usuários |
financial_intake_results | Snapshots de intake enviados |
financial_plans | Registros de planos gerados |
personality_questions | Catálogo de perguntas do quiz |
money_moves_catalog | Biblioteca de templates de movimentos |
Exemplos de fluxo de requisição
Autenticação OTP (camada de 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 }
Geração de plano (roteada pela camada de 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
Completude de chat (com histórico 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
Injeção de contexto
O endpoint de chat aceita um objeto user_context que sua aplicação monta a partir do seu próprio banco de dados antes de cada chamada. Este é o mecanismo principal para passar estado do usuário rico e em tempo real para a camada de inteligência sem que a API precise armazenar nada.
Campos que sua aplicação pode injetar:
- Identidade e preferências:
name,language,personality_type,plan_tier,plan_speed - Snapshot financeiro:
monthly_income,goal_progress,total_saved_cents - Hist órico comportamental:
completed_moves_count,last_confirmation_note,next_move - Sinais de sessão:
entry_point,spending_alert_envelope,spending_alert_ratio - Contexto de recuperação:
rag_context— texto livre do seu próprio pipeline de recuperação
Todos os campos de user_context são opcionais. A camada de inteligência usa o que estiver presente para personalizar a resposta; campos ausentes simplesmente não são referenciados.
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
Roteamento por entry_point
Quando um usuário abre a tela de chat, seu app sabe como ele chegou. Passar entry_point em user_context permite que o Ozzie gere uma mensagem de abertura contextualmente adequada (entry_point_opener) que seu app pode exibir antes de o usuário digitar qualquer coisa.
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
Seu app deve resolver o entry_point a partir dos parâmetros de inicialização da sessão (deep link, payload de notificação, etc.) antes de montar o objeto user_context.
Estados de erro
| Código | HTTP | Descrição |
|---|---|---|
INTAKE_REQUIRED | 422 | O intake financeiro deve ser enviado antes de gerar um plano |
PERSONALITY_REQUIRED | 422 | personality_type deve ser fornecido (se obrigatório pelo endpoint) |
PLAN_REQUIRED | 422 | O plano deve ser gerado antes que este endpoint possa ser chamado |
INVALID_CODE | 400 | Código OTP incorreto ou já utilizado |
CODE_EXPIRED | 400 | Código OTP ultrapassou o TTL de 10 minutos |