Vibe-coded WhatsApp bot — por que ele responde errado em produção (e o checklist de hardening)

Bot WhatsApp gerado no Lovable/Bolt parece pronto, mas quebra em 8 pontos previsíveis quando entra em produção. Aqui o checklist que uso pra blindar antes do primeiro cliente.

A resposta curta, antes da história longa: um bot WhatsApp feito no Lovable/Bolt/v0 responde errado em produção em 8 pontos previsíveis — idempotência de webhook, expiração de media URL, race condition de botão, limite de 4096 caracteres, separação sandbox/produção, contexto de reply, rotação de token push e observabilidade. Esse post é o checklist que eu rodo antes de qualquer bot WhatsApp ir pra cliente pagante. Cada item já me mordeu, eu te conto qual e custou quanto.

Sou o Ulisses, fundo a Hens e construí o OverAir — bot WhatsApp de memória digital com IA, em produção desde início de 2026. 0 pagantes hoje, vou ser honesto com isso o post inteiro. Mas o bot processa minhas mensagens e as de uma dezena de beta-testers todo dia, e os 8 bugs aqui são os mesmos que apareceram em três bots WhatsApp que entreguei pra clientes em 2025 — o tipo de coisa que o agente nunca escreve sozinho.

Se você tá com um bot vibe-coded prestes a entrar em produção, leia até o final. Se o seu bot já tá em produção, abre o git blame em paralelo — provavelmente 5 desses 8 já tão na sua base de bugs sem você ter percebido.

Por que o agente erra exatamente nesses 8 pontos

Lovable, Bolt e v0 são otimizados pra demonstrar fluxo feliz em 2 minutos. O bot vibe-coded responde "olá" pra você no preview, e parece que o trabalho acabou. Não acabou. Tudo que descrevo abaixo é o que a Meta documenta como comportamento normal da Cloud API — entregas duplicadas, URLs efêmeras, retries automáticos — e o agente simplesmente não tem essa documentação no contexto quando gera o handler.

Eu testei: pedi pra três agentes diferentes (Lovable, Bolt, Cursor com Claude) escreverem um webhook handler do WhatsApp Cloud API. Os três retornaram código que assumia entrega exactly-once. A Meta documenta o oposto: "Webhooks are delivered at-least-once, which means duplicates are a normal operating condition, not an edge case" (Hookdeck WhatsApp guide). Não é trivia — é o primeiro bug que entra em produção.

O checklist — 8 itens, na ordem em que me morderam

1. Idempotência por message_id (o que mais quebra cobrança)

O bug: A Meta dispara o mesmo webhook 2× em ~0,5% dos casos. O bot processa o pagamento duas vezes. O cliente vê débito duplicado e abre dispute.

Eu vi isso ao vivo no OverAir num teste de carga interno em fevereiro: dois usuários receberam confirmação dupla porque o checkout.session.completed chegou duas vezes em 800ms. Custo do bug em produção real: US$ 15 por chargeback do Stripe (Stripe dispute fees, 2026). 1.000 transações/mês × 0,5% × US$ 15 = R$ 375/mês evaporados em disputes. Sem contar o cliente irritado.

O hardening: dedup no banco com o messages[].id (inbound) ou statuses[].id (status update) como chave primária. Lovable nem cogita escrever isso. Padrão que uso no OverAir:

// Antes de processar qualquer coisa
const eventRef = db.collection('whatsapp_events').doc(message.id);
const exists = await eventRef.get();
if (exists.exists) {
  // Já processei, retorno 200 sem fazer nada
  return res.status(200).send();
}
// Marca como visto ANTES de processar
await eventRef.set({ receivedAt: FieldValue.serverTimestamp() });
// Agora sim, processa
await handleMessage(message);

Detalhe que o agente ignora: a marcação como visto tem que ser antes do processamento, não depois. Se você marca depois e dois webhooks chegam em paralelo, ambos passam pelo if (exists) antes de qualquer um marcar. Em Firestore, isso resolve com transaction; em Postgres, com INSERT … ON CONFLICT DO NOTHING e check do rowCount.

2. Media URL expira em 5 minutos (silenciosamente)

O bug: Usuário manda áudio. Bot recebe webhook com media.id. Bot enfileira pra processar. Quando o worker pega da fila 7 minutos depois, faz GET na URL e recebe 404. Áudio perdido. Cliente nunca sabe.

Não é especulação — está documentado pela Meta: "All media URLs expire within 5 minutes" (WhatsApp Cloud API media docs). E pra acessar precisa de bearer token no header, então a URL "vaza" nada — mas também não dá pra cachear pra retry.

Num bot que entreguei pra um cliente em 2025 (vou anonimizar — sistema de cobrança via WhatsApp), gastei 6 horas debugando "áudios sumindo" antes de cair a ficha. O dashboard mostrava o webhook chegando, mas o arquivo não estava no S3. Causa: a fila tinha lag de 3 minutos em horário de pico e a URL expirava.

O hardening: baixa o blob dentro do mesmo handler do webhook, antes de enfileirar. Não enfileira a URL — enfileira o byte stream (em S3, GCS, R2). Se sua infra serverless tem timeout curto, o handler deve fazer:

1. Receber webhook
2. Baixar o media imediatamente (max 60s do recebimento)
3. Subir pra storage (S3/GCS/R2)
4. Enfileirar com o STORAGE_URL, não com o media.id
5. Retornar 200 pra Meta

O agente vibe-coded escreve "enfileira o media.id, baixa depois". Em desenvolvimento funciona. Em produção, falha em ~15% dos áudios em horário de pico — o número que medi no OverAir antes do fix.

3. Race condition em botões de aprovação

O bug: Usuário recebe botão "Confirmar agendamento". Clica. Internet trava 2 segundos. Clica de novo. Bot recebe dois callbacks. Sem dedup, agenda duas vezes. Cliente vê duplicação e cancela todo o fluxo.

WhatsApp Cloud API expõe interactive.button_reply como evento normal — chega como webhook, com messages[].id único. Se você não dedupe pelo item 1 desta lista, vai duplicar a ação. Mas tem uma sutileza: o button_reply não traz o context.id da mensagem original do botão por padrão — vem o id da resposta do usuário. Pra correlacionar "qual botão foi clicado de qual mensagem", você precisa salvar o mapeamento payload → wamid na hora de mandar o botão.

Num sistema que entreguei recentemente (bot de aprovação de pedidos), o fluxo certo é:

1. Bot manda mensagem com 3 botões (aprovar/rejeitar/pedir info)
2. Bot persiste {wamid_da_mensagem, conversation_id, estado_atual}
3. Usuário clica
4. Webhook chega com button_reply.payload (o ID que você setou)
5. Bot busca {conversation_id} via payload
6. Bot CHECA o estado atual em TRANSAÇÃO — se já aprovado, ignora
7. Atualiza estado, manda confirmação como reply (item 6 desta lista)

A transação no passo 6 é o que o agente nunca escreve. Sem ela, dois cliques viram dois "aprovou". Eu vi essa race acontecer 2 vezes em 10 testes manuais num bot vibe-coded — não é raro, é o caso comum em conexão móvel ruim.

4. Limite de 4.096 caracteres no body (corte ou explode)

O bug: Bot tem GPT/Gemini gerando resposta longa. Resposta sai com 5.200 caracteres. Cloud API rejeita com (#100) Invalid parameter e o usuário não recebe nada. No log, aparece como sucesso porque o vibe-coded não checou o response.

Limite oficial: 4.096 caracteres no corpo de mensagem de texto (Meta Cloud API reference, Symphony WhatsApp limits). E note: é caracteres UTF-8, não bytes. Emoji ocupa 1 char, mas alguns ZWJ (família 👨‍👩‍👧) ocupam 7+.

O hardening:

function splitForWhatsApp(text: string, maxLen = 4000): string[] {
  if (text.length <= maxLen) return [text];
  const chunks: string[] = [];
  const paragraphs = text.split('\n\n');
  let buffer = '';
  for (const p of paragraphs) {
    if ((buffer + '\n\n' + p).length > maxLen) {
      chunks.push(buffer);
      buffer = p;
    } else {
      buffer = buffer ? buffer + '\n\n' + p : p;
    }
  }
  if (buffer) chunks.push(buffer);
  return chunks;
}

Trabalho em 4.000 (não 4.096) pra ter buffer de segurança contra contagem de emoji. Mando os chunks em sequência com setInterval de 800ms — manda tudo de uma vez e cai na trava de "mensagem muito rápida" do WhatsApp.

5. Sandbox vs produção: o env var é o primeiro lugar do bug

O bug: Deploy pra produção. Bot continua respondendo do número de teste. Cliente real não recebe nada. Demora 2 dias pra alguém perceber porque "no dev tava funcionando".

Causa: a Cloud API exige um phone_number_id específico por número. Sandbox e produção têm IDs diferentes, e o WABA token tem escopos diferentes. Lovable persiste essas vars no ambiente do projeto e quando você "publica", herda o .env errado em 1 de cada 3 deploys (medi num bot vibe-coded de cliente).

O hardening: crie uma smoke check que roda no deploy:

const expected = process.env.NODE_ENV === 'production'
  ? 'PROD_PHONE_NUMBER_ID'
  : 'SANDBOX_PHONE_NUMBER_ID';
if (process.env.WHATSAPP_PHONE_NUMBER_ID !== expected) {
  throw new Error('Wrong WhatsApp number ID for environment');
}

Coloca isso no entrypoint. Falha cedo, ruidosamente. O agente nunca escreve esse guard — ele assume que env é só "set and forget".

6. Reply com context.message_id (senão o usuário se perde)

O bug: Bot recebe pergunta. Bot vai consultar API externa por 4 segundos. Bot responde. Mas o usuário já mandou outra mensagem nesse intervalo. Resposta chega solta no chat, sem relação visual com a pergunta. Usuário acha que o bot tá louco.

A Meta documentou o fix: você pode quotar a mensagem original passando context.message_id no corpo do envio (Meta Cloud API send-messages). Estrutura:

{
  "messaging_product": "whatsapp",
  "to": "5511...",
  "context": { "message_id": "wamid.HBgM..." },
  "type": "text",
  "text": { "body": "Resposta pra sua pergunta de 4s atrás" }
}

Restrição: só funciona pra mensagens com até 30 dias de idade — depois disso a Meta ignora o context e manda solta. Não relevante pro fluxo de bot (você respondendo em segundos), mas vale anotar.

Por que o agente esquece: o agente trata cada send_message como independente. Não tem modelo mental de "essa resposta é continuação dessa pergunta específica". Você precisa explicitar: "sempre que estiver respondendo a uma mensagem do usuário, inclua o context.message_id da mensagem original".

7. Rotação de token FCM (se o bot empurra pro app)

O bug: Bot WhatsApp dispara push notification pro app Flutter via FCM. App fica inativo 60 dias, Firebase rotaciona o token. Bot continua mandando pro token antigo. Recebe messaging/registration-token-not-registered. Vibe-coded ignora o erro. Push silencia.

Esse é específico pra arquiteturas WhatsApp-first com app companion — exatamente o caso do OverAir. Firebase documenta a expiração: tokens FCM podem rotacionar a qualquer momento (Firebase Cloud Messaging best practices). O Lovable não escreve loop de rotação — ele assume que o token salvo no banco é o token atual pra sempre.

O hardening:

  1. Salvar fcm_token com updatedAt.
  2. Quando o app abre, sempre chamar messaging.getToken() e fazer upsert no backend.
  3. No backend, ao mandar push: capturar messaging/registration-token-not-registered e marcar o token como invalid.
  4. Job diário limpa tokens marcados há mais de 7 dias.

Sem isso, em 6 meses 40% dos seus tokens FCM tão mortos e você nem sabe.

8. Observabilidade — ou: você só descobre o bug pelo cliente

O bug: Bot vibe-coded loga console.log em STDOUT. Lovable não pluga em nada. Quando algo quebra em produção, você só sabe porque o cliente reclama 6 horas depois.

Isso não é exagero — eu vivi: bot de cliente quebrou às 23h de uma sexta porque a Meta rotacionou um endpoint interno. Eu só descobri segunda às 9h, pelo WhatsApp do cliente reclamando que "o bot tá mudo desde sexta". 48 horas de downtime em produção, sem alerta nenhum.

O hardening: mínimo viável de observabilidade pra bot WhatsApp:

  • Estruturado em JSON, não console.log de string. Cada log tem event, message_id, wa_user, latency_ms, error_code.
  • Sentry no handler de erro — free tier cobre 5k events/mês (Sentry pricing), suficiente pra bot até 500 clientes.
  • Health check endpoint que o uptimerobot (free) bate a cada 5min. Se cair, SMS pro seu celular.
  • Counter custom de webhook_received, webhook_processed, whatsapp_send_failed. Discrepância entre received e processed = sintoma. Sem essa contagem, você opera no escuro.

A maioria dos bots vibe-coded em produção que auditei rodam sem nenhuma das 4 acima. Não é falta de tempo — é falta de saber que precisa.

A tabela-resumo (pra você imprimir e colar no monitor)

# Item Como o bot vibe-coded quebra Hardening mínimo Custo do bug
1 Idempotência por message_id Processa 2× em ~0,5% dos webhooks Dedup transacional no DB com wamid como PK US$ 15/chargeback Stripe
2 Media URL expira em 5min Áudios/imagens perdidos no pico Baixar dentro de 60s, enfileirar storage URL ~15% de mídia perdida em pico
3 Race condition em botões Dois cliques = duas ações Transação no estado + dedup item 1 Aprovação duplicada
4 Limite de 4.096 chars Mensagem longa explode, falha silenciosa Chunking em 4.000 + delay 800ms entre chunks Resposta sumida
5 Sandbox vs produção phone_number_id errado no deploy Smoke check no entrypoint validando env Cliente real não recebe nada
6 context.message_id no reply Resposta solta confunde usuário Sempre incluir context da pergunta original UX confuso, abandono de fluxo
7 Rotação de token FCM Push silencioso após 60 dias inativos Upsert em getToken() + cleanup de inválidos 40% dos tokens mortos em 6 meses
8 Observabilidade Você descobre pelo cliente JSON log + Sentry + healthcheck + counters 48h de downtime sem alarme

Quanto custa fazer o hardening: 5 dias ou R$ 4.000–6.000

Sou específico de propósito. Pra um bot vibe-coded já em produção, blindar os 8 itens leva 3 a 5 dias de dev sênior se a base não tá uma bagunça. Em São Paulo, 2026, dev freela senior fecha em R$ 800 a R$ 1.200 por dia — então o custo total fica entre R$ 2.400 e R$ 6.000.

Compara com o custo de não fazer:

  • 5 chargebacks/mês × US$ 15 = R$ 375/mês evaporados.
  • 1 cliente irritado/semana pelo bot mudo = LTV de ~R$ 800 cada perdido.
  • 48h de downtime trimestral = reembolso médio de R$ 2.000 pra base de 200 clientes.

Em 3 meses o hardening se paga. Em 12 meses, sem ele, você gasta o triplo em remediação reativa.

O que eu evitaria, com convicção

Não levaria bot WhatsApp vibe-coded pra produção paga sem rodar esse checklist. Pra MVP, pra demo, pra rodar com 5 amigos beta-testando? Tudo bem — Lovable em 2 dias te dá algo que conversa. Pra cobrar mensalidade de cliente que depende daquilo pra agenda de paciente, pra pedido de delivery, pra confirmação de pagamento? Erro caro.

O agente vibe-coded não tem o modelo mental de produção. Ele tem o modelo mental de demo. São coisas diferentes. Se o seu bot vai entrar em produção, ou você revisa esses 8 itens manualmente, ou você contrata alguém que já fez isso 3 vezes — porque é o que eu faço pra cliente Hens e é o que eu fiz pra OverAir antes de abrir pra beta.

Se quer um audit de bot WhatsApp em produção, me chama no WhatsApp da Hens. Cobro fixo R$ 3.500 pelo audit dos 8 itens + relatório acionável em PDF. Sem rodeio.

Fontes

Quer um app assim pro seu negócio?

A Hens constrói apps Flutter, bots WhatsApp com IA e backoffices sob medida — com o mesmo rigor dos nossos produtos próprios. Do MVP ao app em produção. Primeira conversa é direta, sem formulário.

Falar no WhatsApp →