Motor de seleção de ofertas — diagnóstico e proposta¶
Status: implementado (2026-09-29) — fases 0 a 5, com as regras ao vivo em SHADOW (registram o que fariam; quem pausa de verdade continua sendo o spend guard). Regra do projeto mantida: todo número é determinístico (SQL/TS puro); a IA só produz texto qualitativo. Guia de operação na seção 7.
1. O problema em uma frase¶
Hoje a oferta "escolhida" é, na prática, a primeira do ranking do Radar — e esse ranking mede o que o marketplace diz sobre a oferta, não quanto dinheiro esperamos ganhar ou perder colocando tráfego pago nela.
2. Diagnóstico do que existe hoje¶
| Peça | O que faz | Problema para decidir dinheiro |
|---|---|---|
Radar Opportunity Score v3 (packages/integrations/src/clickbank/opportunity-score.ts) |
Soma aditiva de 8 componentes (payout, gravity, funil, fit de tráfego, criativo, compliance, tendência, share Tier-1) | A captura real só preenche payout, avg, recorrência e gravity. Teto prático ≈ 42/100; presets "≥70" e "≥80" do Radar nunca casam. Dado desconhecido vale 0 pontos, então "sem dado" parece "ruim". |
| Gravity | Faixa fixa (sweet spot 20–180) | Usa só o nível do dia. Os snapshots diários de 90 dias existem, mas ninguém calcula tendência. Há duas definições de sweet spot (20–180 e 25–120). |
| CPC alvo | payout × 1,5% × 0,6 |
CVR de 1,5% é suposição fixa; não há dado de mercado (volume de busca, CPC real, concorrência). |
| Opportunity v2 (SQL, SHADOW) | Usa o ledger próprio (coortes maduras) | Não é combinado com o Radar; comentário diz 40/40/20, código usa 50/50; refund_proxy_rate sem sentido; roda só por botão. |
| Cenários / recomendação (SQL) | Economia derivada e tiers TEST/VALIDATE/SCALE | Cenários não têm offer_id; break_even_cpc em scenario_derived_economics = rev_per_click / rate = payout líquido por venda, não um CPC (o CPC de equilíbrio é o próprio rev_per_click). |
Termos do afiliado (lib/offers/affiliate-terms.ts) |
Lê proibições (busca paga, brand bidding, cópia) | Só bloqueia o lançamento no Google; não entra no ranking. |
Spend guard (lib/money/guard-rules.ts) |
Kill switch, teto diário, orçamento de teste, stop-loss = 1× payout sem venda | É a única peça que mexe em dinheiro, e só pausa. O stop-loss é fixo em múltiplo do payout, sem considerar CPC, CVR esperada ou atraso de atribuição. |
| OODA / traffic-policy (TS + SQL) | Regras de SCALE/PAUSE | Não é chamado por nenhuma rota. |
Bugs pontuais encontrados no caminho (corrigir antes de qualquer motor novo):
OpportunityScoreBadge.tsx:24testaradar-opportunity-v2; linhas v3 mostram máximos antigos (25/20/20).- Caminho do feed (
marketplace-sync.ts) não passaavgPayoutao score. break_even_cpcdo cenário (acima).- Colunas
averageEPC/initialEPC/futureEPC/netEPCdo grid são gravadas como $/venda — semântica não verificada. Conferir 3 ofertas à mão contra a tela do ClickBank antes de confiar em qualquer cálculo.
3. Princípio do motor novo¶
Marketplace é prior, não decisão. Gravity e payout dizem que alguém ganha dinheiro com a oferta; não dizem se nós ganhamos, no nosso canal (Google Search), com o nosso CPC. A decisão certa é:
Investir onde a probabilidade de lucro × tamanho do lucro é maior, com o menor custo para descobrir que estávamos errados — e atualizar essa crença com cada clique e cada venda.
Isso se traduz em três camadas: portões (nunca investir), valor esperado (onde investir primeiro) e aprendizado (quanto continuar investindo).
4. Pipeline proposto¶
Universo (~1.237 ofertas capturadas)
│ 1. Portões determinísticos (grátis)
▼
~100–200 elegíveis
│ 2. Validação de mercado no Google (Keyword Planner, com cache)
▼
~20 viáveis em Search
│ 3. Revisão qualitativa (IA só texto + humano): página, claims, ângulos
▼
Shortlist com plano de teste (orçamento e critério de parada definidos ANTES)
│ 4. Teste ao vivo com regras bayesianas (SHADOW → ativo)
▼
Escala / pausa ──► alimenta os priors por categoria (volta para o passo 2)
4.1 Portões (bloqueio, não pontuação)¶
Uma oferta que falha em um portão não recebe score — some do ranking com o motivo.
| Portão | Fonte | Regra inicial |
|---|---|---|
| Termos proíbem busca paga | offers.terms_prohibit_paid_search |
true → bloqueia Search; nunca lidos → nunca "pronta" |
| Página de vendas | sales-page resolver | não encontrada/falhou → revisar (o resolvedor não prova que está fora do ar) |
| Compliance | regras de categoria + brief | VERY_HIGH → bloqueia Search; HIGH → só com revisão humana |
| Economia mínima | payout líquido | V_net < US$ 30 → bloqueia (o CPC de equilíbrio fica abaixo do mínimo praticável) |
| Gravity | snapshot | < 5 (sem validação) ou > 350 (leilão saturado) → bloqueia por padrão, configurável |
| Demanda (após passo 2) | Keyword Planner | volume mensal somado das palavras de marca/review abaixo do mínimo → bloqueia |
| Margem de CPC (após passo 2) | break-even vs CPC de mercado | CPC_mercado > CPC_equilíbrio → bloqueia |
Brand bidding proibido não bloqueia, mas troca o conjunto de palavras por genéricas (CPC maior, CVR menor) — o passo 2 precisa ser calculado com o conjunto de palavras que realmente poderemos usar.
4.2 Economia por venda (determinística)¶
V_net = avg_payout × (1 − refund_prior(categoria)) # $/venda líquida
CVR_prior = hop→venda do marketplace (quando houver) × CTR presell→hop prior
CPC_eq = CVR_prior × V_net # CPC de equilíbrio
CPC_mercado= mediana(low/high top-of-page bid) ponderada por volume (Keyword Planner)
Margem = 1 − CPC_mercado / CPC_eq
ROAS_prior = CVR_prior × V_net / CPC_mercado
refund_prior começa como tabela por categoria (configurável, com fonte e data)
e passa a ser aprendida do ledger (performance_ledger_entries) quando houver
coortes maduras.
4.3 Tendência de mercado (dados que já temos)¶
Dos snapshots diários (90 dias), por identity_key:
- inclinação de log(gravity) em 14 e 30 dias (regressão linear) → momentum;
- dias no marketplace (primeiro
snapshot_date) → ofertas muito novas têm variância maior; - volatilidade de gravity e de payout → instabilidade do produtor;
- mudança de rank.
Leitura: gravity moderada subindo = outros afiliados validando agora (bom); gravity alta e caindo = oferta cansando; gravity muito alta estável = leilão de marca disputado (CPC alto — o passo 2 confirma).
4.4 Ranking: valor esperado com incerteza¶
Em vez de somar pontos, cada oferta elegível recebe:
- P(lucro) = probabilidade de
ROAS > 1dada a incerteza dos insumos (CVR como Beta; CPC e refund como faixas). Calculado por fórmula fechada ou simulação determinística com semente fixa (reprodutível, em SQL/TS). - Lucro esperado por US$ 100 de teste =
E[ROAS − 1] × 100. - Custo de descobrir = orçamento de teste necessário (4.5).
- Confiança dos dados = quais insumos são observados vs supostos (explícito, nunca escondido como zero).
Ordenação padrão: P(lucro) × E[lucro] com desempate por menor custo de
descobrir. A tela mostra por que cada oferta está no topo e o que pode dar errado.
4.5 Plano de teste antes de gastar¶
Toda oferta aprovada sai com o plano escrito:
- Orçamento de teste =
k × V_net, comkescolhido para que, se a oferta estiver exatamente no equilíbrio, a chance de ver 0 vendas seja aceitável:P(0 vendas) ≈ e^(−k)→k=1: 37% (mata muita oferta boa),k=2: 14%,k=3: 5%. Hoje o stop-loss ék=1. - Critério de pausa e de escala (4.6) mostrado ao operador antes do "Ativar".
- Diversificação: no máximo N testes simultâneos e no máximo 1–2 por categoria (falhas na mesma categoria são correlacionadas: política do Google, sazonalidade).
- Tudo limitado pelo capital disponível (
capital_calculation_snapshots) e pelos limites deworkspace_spend_limits(que continuam como teto absoluto).
4.6 Aprendizado ao vivo (bayesiano)¶
Por oferta/campanha, com cliques c e vendas s atribuídas (INS):
prior CVR ~ Beta(a0, b0) média = CVR_prior, força = 50–150 cliques-equivalentes
posterior CVR ~ Beta(a0 + s, b0 + c − s)
CVR* = CPC_real / V_net # CVR de equilíbrio
P(lucro) = 1 − BetaCDF(CVR*; a0 + s, b0 + c − s)
Regras (começam em SHADOW, comparadas com o que o spend guard faria):
| Condição | Ação |
|---|---|
P(lucro) < 10% e gasto ≥ 1× V_net |
PAUSAR |
P(lucro) > 90% e s ≥ 3 vendas com janela de atribuição fechada |
ESCALAR +20% (respeitando max_increase_pct_24h) |
| senão | MANTER |
Exemplo: V_net = US$ 120, CPC real US$ 1,20 → CVR* = 1,0%. Prior Beta(1, 99)
(média 1%). Após 150 cliques (US$ 180) sem venda: posterior Beta(1, 249),
P(CVR > 1%) = 0,99^249 ≈ 8% → pausa. Com 1 venda nos mesmos 150 cliques:
Beta(2, 248) → P ≈ 29% → mantém. A regra se ajusta sozinha ao CPC e ao
payout, o que o stop-loss fixo não faz.
Cuidados:
- Atraso de atribuição: vendas do ClickBank chegam horas/dias depois do clique; só contar cliques com janela mínima (ex.: 72 h) no denominador.
- Refund chega em até 60 dias: escala usa
V_netcom prior de refund até a coorte amadurecer. - Sinais antecedentes para diagnosticar antes da venda: CTR do anúncio, CTR presell→hop, qualidade dos termos de busca. Separam "oferta ruim" de "tráfego/funil ruim" — pausar a oferta por um problema de presell é o erro mais caro.
- O stop-loss atual continua como teto duro; o motor só pode ser mais conservador que ele até ser promovido.
Os resultados de cada teste atualizam os priors por categoria (CVR, refund, CPC), então a próxima rodada de seleção começa mais bem informada.
4.7 Medir o próprio motor¶
Cada recomendação vira uma linha append-only com todos os insumos e a versão da
fórmula (padrão já usado em opportunity_score_snapshots). Depois, comparar
previsão × resultado: calibração de P(lucro), acerto do decil superior,
custo médio por oferta descartada. Sem isso não dá para saber se o motor é
melhor que "pegar a primeira do ranking".
5. Dados que faltam e como obter¶
| Dado | Fonte | Custo |
|---|---|---|
| Volume de busca, CPC low/high, concorrência | Google Ads KeywordPlanIdeaService (generateKeywordIdeas, generateKeywordHistoricalMetrics) — mesma credencial/developer token (acesso Basic) |
Cota da API; cachear por palavra/país/mês em tabela keyword_market_snapshots e consultar só as elegíveis do passo 1 |
| Refund por categoria | Prior manual → ledger próprio | Zero |
| Tendência de gravity | Snapshots já gravados | Zero (view SQL) |
| Rebill real / funil | Não disponível no grid; página de vendas + JV page | Captura/IA qualitativa |
Semântica das colunas *EPC |
Conferência manual de 3 ofertas | 15 min |
6. Plano de implementação por fases¶
| Fase | Entrega | Critério de pronto |
|---|---|---|
| 0 — Correções | Bugs da seção 2; presets do Radar relativos ao conjunto (percentis 75/90); sweet spot único | Testes + pgTAP |
| 1 — Dados que já temos | View de tendência de gravity; tabela de priors de refund por categoria; V_net; portões de termos/página/compliance no ranking |
Cada oferta bloqueada mostra o motivo |
| 2 — Demanda Google | Integração Keyword Planner com cache e rate limit; CPC_mercado, margem e portão de viabilidade em Search |
Shortlist com margem calculada para ≥ 90% das elegíveis |
| 3 — Ranking por valor esperado | offer-decision/v1 (TS puro + espelho SQL), P(lucro), plano de teste, tela "Por que esta oferta" |
Recomendações gravadas append-only com insumos |
| 4 — Aprendizado bayesiano | Regras 4.6 em SHADOW ao lado do spend guard; priors por categoria atualizados pelo ledger | 2–4 semanas em SHADOW com relatório de divergências antes de promover |
| 5 — Calibração | Painel previsão × resultado | Calibração revisada antes de aumentar orçamento |
Fases 0–1 não dependem de nada externo e já mudam a qualidade da escolha. A fase 2 é a que mais muda o jogo para Google Search: sem demanda e CPC reais, qualquer score de oferta é um palpite sobre o canal.
7. Implementação e operação¶
Onde está cada peça¶
| Peça | Arquivo |
|---|---|
| Beta, priors, motor de oferta, regra ao vivo, Keyword Planner (puro) | packages/integrations/src/decision/ |
Método generateKeywordHistoricalMetrics |
packages/integrations/src/google/client.ts |
Correções da fase 0 (break_even_cpc, refund_rate) |
supabase/migrations/20260929100000_brandvelia_decision_phase0_fixes.sql |
| Tendência, cache do Keyword Planner, trilha de decisões, contexto/registro ao vivo, calibração, retenção | supabase/migrations/20260929100100_brandvelia_offer_decision_engine.sql |
| Montagem dos insumos, aprendizado por categoria, executores | apps/web/src/lib/decision/ |
| Ações (recalcular, buscar no Google, configuração, avaliar ao vivo) | apps/web/src/lib/actions/offer-decisions.ts |
| Tela | /app/decisions (menu Decisão) |
| Crons | /api/cron/offer-decisions (diário) e /api/cron/traffic-control (regra ao vivo em SHADOW a cada rodada) |
| Testes de banco | supabase/tests/brandvelia_offer_decision_engine.sql |
Fluxo do operador¶
- Capture o marketplace (Radar) — o cron diário recalcula; ou clique em Recalcular decisões.
- Em Decisão, as ofertas "Falta dado do Google" estão ranqueadas com CPC estimado pela categoria. Selecione até 10 do topo e clique em Buscar demanda e CPC no Google (exige a conta Google Ads ativa, developer token com acesso Basic e, se a conta não for em USD, a cotação do dólar em Meu orçamento).
- Ofertas Prontas para teste já trazem orçamento, CPC máximo e critério de parada. Termos do produtor nunca lidos impedem o "pronta": cadastre a oferta para o sistema buscá-los.
- Depois de ativar, a seção Testes ao vivo mostra o que a regra bayesiana faria, lado a lado com o que o controle de gasto fez. Calibração compara a chance prevista com o resultado das campanhas maduras.
Decisões de implementação¶
- Trilha deduplicada pela decisão, não pelos insumos: a gravity oscila todo dia; gravar cada oscilação seria ~1 linha por oferta por dia. A assinatura (status, códigos, P(lucro) em pontos percentuais, EV em dólares) só muda quando a decisão muda. Retenção de 180 dias, preservando a última decisão de cada oferta e a usada na calibração de cada campanha.
- Sem dupla contagem no ao vivo: o prior de uma campanha é a crença antes de ativar (a decisão de oferta da época) ou o prior editorial da categoria — nunca o aprendido, que já inclui os cliques da própria campanha.
- Keyword Planner com escopo certo: marca proibida pelos termos → a busca usa só palavras genéricas, e um snapshot de marca é ignorado (mediria o leilão errado). Snapshot vale 90 dias.
- Promoção do SHADOW: só depois de 2–4 semanas comparando "regra faria" × "controle de gasto fez" na tela, e com a calibração revisada. Nada neste motor pausa, ativa ou escala.