CRO 360 — Testes A/B e Personalização Criados por Agentes de IA, Aprovados por Humanos
Plataforma SaaS multitenant onde um agente de IA inspeciona o site, propõe um teste A/B ou uma personalização dentro de um contrato seguro, e um humano revisa e publica. A execução acontece no edge, em milissegundos, sem JavaScript arbitrário.
Problema
Times de CRO travam em dois gargalos. O primeiro é de capacidade: cada teste A/B exige briefing, fila de desenvolvimento e QA, então a empresa roda 2 ou 3 testes por mês quando precisaria rodar 20. O segundo é de confiança: as ferramentas que aceleram isso costumam injetar JavaScript arbitrário na página, sem revisão, sem rastro de quem publicou o quê — o que é inaceitável em qualquer operação séria. E com IA no meio, o risco só aumenta: ninguém quer um agente com permissão de escrever código solto no site em produção.
Solução
O CRO 360 separa quem propõe de quem publica. Um agente de IA conectado via MCP inspeciona as páginas do domínio autenticado e cria um rascunho dentro de um contrato declarativo — não escreve código, preenche uma especificação validada por JSON Schema. Um humano revisa esse rascunho com preview ao vivo por variante, aprova e publica. Só então um bundle assinado vai para o edge da Cloudflare, onde a decisão acontece em milissegundos.
Resultados
Em produção: ciclo completo de rascunho por IA → revisão humana → publicação no edge, com preview ao vivo por variante, screenshots automáticos via Playwright, isolamento multitenant no PostgreSQL e audit log imutável de toda transição. O MCP não tem escopo de aprovação nem de publicação — por design, não existe caminho para um agente burlar a revisão humana.
CRO 360
O objetivo
Deixar que um agente de IA faça o trabalho pesado de propor testes A/B e personalizações, sem nunca abrir mão da revisão humana antes de qualquer coisa ir ao ar.
A pergunta que originou o projeto foi bem específica: dá pra colocar uma IA para criar experimentos em um site de produção sem que isso vire um risco de segurança? A resposta foi sim — mas só se a IA nunca escrever código, e só se a publicação for sempre um ato humano explícito.
O problema que ele resolve
Gargalo de capacidade
Um teste A/B tradicional passa por briefing, priorização, fila de desenvolvimento, QA e deploy. Na prática, times enxutos rodam 2 ou 3 testes por mês. A limitação não é falta de hipótese — é o custo de transformar hipótese em experimento no ar.
Gargalo de confiança
As ferramentas que aceleram esse ciclo geralmente resolvem por injeção de JavaScript arbitrário: um editor visual que gera um blob de código executado na página do cliente. Isso é rápido e é exatamente o que áreas de segurança bloqueiam — sem revisão de código, sem rastro de autoria, sem garantia do que roda no navegador do usuário final.
Colocar um LLM nesse fluxo, do jeito ingênuo, multiplica o problema: agora você tem uma entidade não determinística com permissão de escrita no seu site em produção.
Como foi feito
A ideia central: contrato, não código
O agente de IA não escreve JavaScript. Ele preenche uma especificação declarativa — o
ExperimentSpec v1, um JSON Schema 2020-12 que é a fonte canônica consumida pela API,
pelo MCP, pelo OpenAPI e pelos testes.
As mudanças permitidas são de cinco tipos, e só esses cinco:
- texto — trocar o conteúdo textual de um elemento;
- estilo — propriedades de um allowlist, com estados opcionais
hover/focus/active; - atributo — atributos permitidos;
- classe — adicionar/remover classes (aceita variantes Tailwind);
- insert — criar elementos e seções inteiras por template sanitizado.
O que é rejeitado na validação, sempre: JavaScript arbitrário, innerHTML, HTML executável,
URLs javascript: e data:. Valores de estilo não podem conter {} ou ;, então nunca
escapam da regra CSS gerada. Imagens e assets só podem apontar para a própria origem do
site — host externo retorna ASSET_HOST_NOT_ALLOWED.
O insert é o mais interessante de implementar com segurança: ele monta a árvore com
createElement + textContent (nunca innerHTML), com tags em allowlist, limite de
6 níveis e 60 nós, e é idempotente — reaplicar não duplica.
O MCP: o que o agente pode e não pode fazer
O agente se conecta por MCP Streamable HTTP (JSON-RPC 2.0), com tools/list publicando
inputSchema e outputSchema completos — o agente não precisa adivinhar formato. As tools
disponíveis:
| Tool | Scope | O que faz |
|---|---|---|
site.get_installation |
site:read |
dados do único site da credencial |
site.inspect_page |
site:read |
lê HTML estático só na origem cadastrada |
experiences.list_active |
experiences:read |
experiências ativas do site |
experience.validate_draft |
drafts:write |
valida sem escrever, erros por campo |
experience.create_draft |
drafts:write |
cria somente rascunho |
preview.get |
preview:read |
lê links de preview e screenshots |
Repare no que não está na lista: não existe tool de aprovar nem de publicar. Isso não é uma configuração que alguém pode ligar — é ausência de código. O agente não tem como burlar a revisão humana porque o caminho não existe.
A credencial fica presa para sempre ao organization_id e site_id do registro. Qualquer
parâmetro que o agente mande no corpo tentando trocar de tenant é ignorado: o escopo vem da
credencial, nunca do payload.
claude mcp add --transport http cro360-site \
"https://.../v1/mcp/CLIENT_ID" \
--header "Authorization: Bearer SEGREDO_EXIBIDO_UMA_VEZ"
O ciclo humano
rascunho (IA) → enviado p/ revisão → revisão concluída → aprovado → publicado

Cada transição vira uma linha imutável no log de governança — quem fez, o quê e quando.
Cada seta é uma ação humana explícita, com RBAC: analyst envia e conclui revisão;
apenas approver e admin aprovam e publicam; viewer é somente leitura. Toda transição
grava organização, experiência, ator e horário no audit_log.
No momento em que o rascunho vai para revisão, o Control API publica um bundle de preview assinado numa chave KV separada, com TTL de 7 dias, e gera um link por variante com token HMAC. O revisor abre a página real com aquela variante aplicada, com badge visual, e sem disparar eventos de exposição no dataLayer — preview não contamina o dado.
Em paralelo, um Cloud Run Job com Playwright captura screenshots de control e variantes por device declarado, grava PNGs num bucket privado, e o console exibe as thumbnails via proxy autenticado por tenant.
A arquitetura
GitHub main
│ GitHub Actions + OIDC/WIF (sem chave estática)
├──> Cloud Run: cro360-console
├──> Cloud Run: cro360-control-api ──> Cloud SQL PostgreSQL
│ ├──> Secret Manager
│ └──> Cloudflare KV (publicação)
└──> Cloudflare Worker + Assets
├──> KV PUBLISHED_CONFIGS
Site do tenant ──SmartCode──> /v1/decision/:siteKey
└──> Queue operacional ──> /v1/internal/health
Agente LLM ──Bearer/site──> /v1/mcp/:clientId
GA4 <── dataLayer de exposição/conversão no navegador
A regra de separação é rígida: o Control Plane é a única autoridade de escrita. O Worker
no edge só lê bundles já publicados e assinados. O navegador nunca escolhe o tenant por query
string — a siteKey resolve a configuração publicada, e a origem da requisição precisa
constar em allowed_origins.

O onboarding do domínio: conectar, instalar o SmartCode, publicar a tag no GTM e validar — sem nenhum registro de DNS.
O monorepo separa por responsabilidade: control-api (HTTP, auth, banco, MCP),
control-plane (núcleo de autorização testável), edge-runtime (Worker), preview-runner
(job Playwright), mais os pacotes contracts, decision-engine e smartcode.
Multitenancy de verdade
Toda consulta e escrita é filtrada por organization_id. Nenhum ID enviado no corpo troca o
tenant resolvido pela credencial. FKs são compostas por (organization_id, id).
E uma regra que exigiu cuidado extra: existe no máximo um MCP ativo por site. Está
garantida em três camadas — na aplicação, com advisory lock do PostgreSQL, e com o índice
parcial uq_mcp_clients_one_active_per_site. Não adianta corrida de requisições.
Validação sem DNS
Conectar um domínio normalmente pede registro TXT no DNS, o que trava o onboarding por dias. Aqui o backend lê o HTML público da página e cruza três sinais: o SmartCode instalado, o container do GTM, e o marcador operacional da tag CRO 360. Passou nos três, o domínio está verificado — e só então o MCP pode ser criado.
O SmartCode no navegador
Um script único servido pelo Worker, instalado como primeiro item do <head>:
<script
src="https://.../cro360-smartcode.v1.js"
data-site-key="SITE_KEY"
data-edge-base="https://..."
data-consent="granted"></script>
Ele faz decisão determinística com sticky assignment (o mesmo visitante cai sempre no mesmo
grupo, por identificador first-party), respeita prioridade, slots e holdout entre experiências
concorrentes, e reaplica as mudanças após navegação SPA e re-render de framework — via
MutationObserver e hooks de history. Anti-flicker é opt-in por data-antiflicker="true".
O padrão de tráfego é conservador de propósito: 10% da audiência elegível, com 5% variante A, 5% variante B e 90% fora do teste.
Privacidade como restrição de arquitetura
Essa parte foi decidida cedo e amarrou o resto do desenho. O CRO 360 não armazena pageviews, conversões, receita ou qualquer resultado analítico de visitante. O GA4 do próprio cliente continua sendo a fonte de performance — a plataforma só guarda configuração, auditoria, identidade administrativa e saúde operacional sanitizada.
O CRO360.identify() aceita nome, e-mail, telefone e ID do usuário logado, mas esses campos
ficam em sessionStorage no navegador e não são enviados à API, ao Worker, à Queue ou ao
PostgreSQL. A personalização usa {{cro360.profile.first_name}} resolvido localmente. Para o
edge segue apenas contexto não identificável e booleanos como is_logged_in. Geolocalização
só existe como segmento coarse e consentido.
Consentimento negado significa nenhum cookie ou localStorage persistente — apenas estado de sessão.
Stack
GCP: Cloud Run (console, control-api), Cloud SQL PostgreSQL 16, Cloud Run Jobs (Playwright),
Cloud Storage, Secret Manager, Workload Identity Federation
Cloudflare: Workers, Workers KV, Queues + DLQ, Workers Assets
Contratos: JSON Schema 2020-12, OpenAPI 3.1, MCP Streamable HTTP, llms.txt
CI/CD: GitHub Actions com OIDC, deploy consolidado por merge em main
O que ainda não está pronto
Prefiro ser direto sobre isso:
- o screenshot de preview captura o viewport (1366×768 e 390×844); elemento abaixo da dobra ainda exige o link ao vivo;
- diff visual, detecção de seletor inexistente, JS errors e Web Vitals no preview: não construídos;
site.inspect_pagelê HTML estático — página renderizada só por JS vai precisar de um executor de browser;- o MCP usa Bearer estático por domínio (armazenado só como hash, com rotação no console). OAuth 2.1 é o próximo marco;
- billing, autosserviço de conta e recuperação de senha: no backlog;
- não existe ambiente de staging implantado.
Quer testar antes de todo mundo?
Estou abrindo um grupo pequeno de primeiros testers — pessoas que vão usar a plataforma de verdade, com acompanhamento direto meu na configuração e no primeiro caso de uso real. Em troca, quero feedback honesto sobre o que funciona e o que ainda não funciona.
Se isso faz sentido pro seu time:
👉 Falar comigo no WhatsApp — (11) 96552-1585
Manda uma mensagem dizendo qual é o seu cenário e eu respondo com os próximos passos.