Ir para o conteúdo

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):

  1. OpportunityScoreBadge.tsx:24 testa radar-opportunity-v2; linhas v3 mostram máximos antigos (25/20/20).
  2. Caminho do feed (marketplace-sync.ts) não passa avgPayout ao score.
  3. break_even_cpc do cenário (acima).
  4. Colunas averageEPC/initialEPC/futureEPC/netEPC do 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 > 1 dada 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, com k escolhido 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 de workspace_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_net com 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

  1. Capture o marketplace (Radar) — o cron diário recalcula; ou clique em Recalcular decisões.
  2. 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).
  3. 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.
  4. 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.