Pontua a intencao de compra dos teus visitantes sem CRM pesado
Queres saber quem dos teus visitantes está mesmo prestes a comprar — sem instalar um CRM a 200 €/mês e passar três dias a ligar workflows. Boa notícia: um simples pixel first-party (um script JS que pões no teu site) basta para captar os sinais que contam. O resto é aritmética ponderada que pões a correr em SQL ou num cron.
Os sinais que valem alguma coisa (e os que podes deitar fora)
O erro clássico: rastrear 40 eventos e afogar o sinal. Na realidade, a intenção de compra lê-se num punhado de comportamentos. Aqui estão os que realmente correlacionam com uma conversão, por ordem de peso decrescente.
- Visita à página de preços: o sinal n.º 1. Quem abre `/pricing` já se projeta como cliente. Pondera-o forte.
- Retorno multi-sessão: 3 sessões em 7 dias = curiosidade que se torna necessidade. Uma só sessão, mesmo longa, vale menos que três curtas espaçadas.
- Tempo ativo (não tempo de página): mede o tempo envolvido (separador visível + scroll/clique), não o separador deixado aberto durante o almoço.
- Profundidade de scroll nas páginas-chave: 80 % de scroll numa página de produto ou num caso de cliente > 100 % de scroll no blog.
- Páginas 'bottom-funnel' vistas: docs de integração, página de segurança, comparativo vs. concorrente, FAQ de faturação. São as perguntas de um comprador, não de um curioso.
- Sinais falsos a ignorar: um único pageview na home, o tráfego referido a partir de um agregador, os bots (filtra por user-agent e por sessões <2s).
O pixel first-party: o que pões, o que envias
First-party = o teu próprio endpoint, o teu próprio cookie. Sem dependência de um SaaS de terceiros, sem bloqueio pelos adblockers de terceiros, e continuas dono dos dados (RGPD-friendly se ficares em intenção anónima + consentimento). Em concreto, um script leve que envia um batimento (heartbeat) quando o separador está visível, e um evento em cada página-chave.
// pixel.js — ~30 lignes, posé sur toutes tes pages
const vid = localStorage.getItem('vid') || crypto.randomUUID();
localStorage.setItem('vid', vid);
function send(type, meta = {}) {
navigator.sendBeacon('/collect', JSON.stringify({
vid, type, path: location.pathname, ts: Date.now(), meta
}));
}
send('pageview');
// temps ACTIF : on n'incrémente que si l'onglet est visible
let active = 0;
setInterval(() => {
if (document.visibilityState === 'visible') {
active += 5;
if (active % 30 === 0) send('heartbeat', { active });
}
}, 5000);
// profondeur de scroll, envoyée une seule fois par palier
let maxScroll = 0;
window.addEventListener('scroll', () => {
const d = Math.round((scrollY + innerHeight) / document.body.scrollHeight * 100);
if (d >= maxScroll + 25) { maxScroll = d; send('scroll', { depth: d }); }
}, { passive: true });
Do lado servidor, `/collect` faz um `INSERT` em bruto numa tabela `events(vid, type, path, ts, meta_jsonb)`. É tudo. Não calculas nada em tempo real: armazenas eventos em bruto e pontuas em batch. Isso poupa-te bugs de dupla contagem e podes re-pontuar todo o histórico quando mudas a ponderação.
A fórmula de scoring: pondera, limita, desvaloriza
O score de um visitante = soma de pontos por sinal, limitada por sinal (senão um só maníaco do scroll falseia-te tudo) e desvalorizada no tempo (uma visita aos preços de há 20 dias já não vale uma visita de ontem). Aqui está uma grelha de partida — calibra-a depois com as tuas conversões reais.
- Visita `/pricing`: +25 por visita, limite 50.
- Página bottom-funnel (docs, segurança, comparativo): +15 cada, limite 45.
- Sessão distinta (>30 min de intervalo): +10, limite 40 (= 4 sessões).
- Bloco de 2 min de tempo ativo: +5, limite 30.
- Scroll ≥75 % em página de produto/caso: +8, limite 16.
- Desvalorização temporal: multiplica o score de cada evento por `0.9 ^ (dias_decorridos)`. Um evento de 7 dias pesa ~48 % do seu valor inicial.
-- score par visiteur, recalculé chaque heure en cron
WITH scored AS (
SELECT vid,
LEAST(SUM(CASE WHEN type='pageview' AND path='/pricing' THEN 25 END), 50) AS pricing,
LEAST(SUM(CASE WHEN path = ANY(ARRAY['/docs','/security','/vs']) THEN 15 END), 45) AS funnel,
LEAST(COUNT(DISTINCT date_trunc('hour', to_timestamp(ts/1000))) * 10, 40) AS sessions
FROM events
WHERE to_timestamp(ts/1000) > now() - interval '30 days'
GROUP BY vid
)
SELECT vid, COALESCE(pricing,0)+COALESCE(funnel,0)+COALESCE(sessions,0) AS score
FROM scored ORDER BY score DESC;
O limiar 'lead quente' e o momento certo para contactar
Um limiar absoluto ("80 = quente") envelhece mal. Prefere um limiar relativo: o top 10 % dos scores do mês móvel. Auto-calibra-se quando o teu tráfego muda. Em concreto, calcula o percentil 90 dos scores ativos e trata tudo o que o ultrapasse como um lead quente a tratar dentro de 24 h.
- Demasiado cedo: contactar logo na primeira visita aos preços = assustas alguém que estava só a fazer benchmark. Espera por um segundo sinal forte (retorno OU página bottom-funnel).
- Janela ideal: o score cruza o percentil 90 E o último evento < 48 h. A intenção está madura e a memória da tua marca está fresca.
- Demasiado tarde: score alto mas último evento > 10 dias = comboio perdido. Contacta na mesma, mas com um ângulo 'lançámos X entretanto', não 'pronto para comprar?'.
- Canal: score quente + email conhecido (newsletter, teste) = email pessoal em 24 h. Score quente anónimo = retargeting leve ou banner in-app, sem contacto direto possível.
Começa mínimo: põe o pixel esta semana, armazena os eventos em bruto, lança a consulta de scoring em read-only e vê-a correr 10 dias sem contactar ninguém. Verás depressa se os teus scores batem certo com a realidade das tuas assinaturas. Só então ligas o alerta de Slack e ajustas os pesos nas tuas conversões reais. Um scoring de intenção útil cabe numa tabela de eventos, numa consulta SQL e num cron — o CRM mamute, guarda-o para quando tiveres uma equipa comercial para alimentar.
A newsletter
Ao subscreveres aceitas receber a newsletter da Zylior. Cancelamento em 1 clique em cada email.