Puntua la intencion de compra de tus visitantes sin CRM pesado
Quieres saber quién de tus visitantes está de verdad a punto de comprar — sin instalar un CRM de 200 €/mes ni pasar tres días cableando workflows. Buena noticia: un simple pixel first-party (un script JS que pones en tu sitio) basta para captar las señales que importan. El resto es aritmética ponderada que haces correr en SQL o en un cron.
Las señales que valen algo (y las que puedes tirar)
El error clásico: trackear 40 eventos y ahogar la señal. En realidad, la intención de compra se lee en un puñado de comportamientos. Estos son los que correlacionan de verdad con una conversión, por orden de peso decreciente.
- Visita a la página de precios: la señal n.º 1. Quien abre `/pricing` ya se proyecta como cliente. Pondérala fuerte.
- Retorno multi-sesión: 3 sesiones en 7 días = curiosidad que se vuelve necesidad. Una sola sesión, aunque sea larga, vale menos que tres cortas y espaciadas.
- Tiempo activo (no tiempo de página): mide el tiempo comprometido (pestaña visible + scroll/clic), no la pestaña dejada abierta durante el almuerzo.
- Profundidad de scroll en páginas clave: 80 % de scroll en una página de producto o un caso de cliente > 100 % de scroll en el blog.
- Páginas 'bottom-funnel' vistas: docs de integración, página de seguridad, comparativa vs. competidor, FAQ de facturación. Son las preguntas de un comprador, no de un curioso.
- Señales falsas a ignorar: un solo pageview en la home, el tráfico referido desde un agregador, los bots (filtra por user-agent y por sesiones <2s).
El pixel first-party: lo que pones, lo que envías
First-party = tu propio endpoint, tu propia cookie. Sin dependencia de un SaaS de terceros, sin bloqueo por los adblockers de terceros, y sigues siendo dueño del dato (GDPR-friendly si te quedas en intención anónima + consentimiento). En concreto, un script ligero que envía un latido (heartbeat) cuando la pestaña está visible, y un evento en cada página clave.
// 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 });
Del lado servidor, `/collect` hace un `INSERT` en bruto en una tabla `events(vid, type, path, ts, meta_jsonb)`. Eso es todo. No calculas nada al vuelo: almacenas eventos en bruto y puntúas en batch. Eso te ahorra bugs de doble conteo y puedes re-puntuar todo el histórico cuando cambias la ponderación.
La fórmula de scoring: pondera, limita, descuenta
El score de un visitante = suma de puntos por señal, limitada por señal (si no, un solo obseso del scroll te lo falsea todo) y descontada en el tiempo (una visita a precios de hace 20 días ya no vale una visita de ayer). Esta es una rejilla de partida — calíbrala luego con tus conversiones reales.
- Visita `/pricing`: +25 por visita, tope 50.
- Página bottom-funnel (docs, seguridad, comparativa): +15 cada una, tope 45.
- Sesión distinta (>30 min de diferencia): +10, tope 40 (= 4 sesiones).
- Tramo de 2 min de tiempo activo: +5, tope 30.
- Scroll ≥75 % en página de producto/caso: +8, tope 16.
- Descuento temporal: multiplica el score de cada evento por `0.9 ^ (días_transcurridos)`. Un evento de 7 días pesa ~48 % de su 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;
El umbral 'lead caliente' y el momento justo para contactar
Un umbral absoluto ("80 = caliente") envejece mal. Prefiere un umbral relativo: el top 10 % de los scores del mes móvil. Se auto-calibra cuando tu tráfico cambia. En concreto, calcula el percentil 90 de los scores activos y trata todo lo que lo supere como un lead caliente a atender en 24 h.
- Demasiado pronto: contactar en la primera visita a precios = espantas a alguien que solo estaba haciendo benchmark. Espera una segunda señal fuerte (retorno O página bottom-funnel).
- Ventana ideal: el score cruza el percentil 90 Y el último evento < 48 h. La intención está madura y el recuerdo de tu marca está fresco.
- Demasiado tarde: score alto pero último evento > 10 días = tren perdido. Contacta igualmente, pero con un ángulo 'hemos sacado X desde entonces', no '¿listo para comprar?'.
- Canal: score caliente + email conocido (newsletter, prueba) = email personal en 24 h. Score caliente anónimo = retargeting ligero o banner in-app, sin contacto directo posible.
Empieza mínimo: pon el pixel esta semana, almacena los eventos en bruto, lanza la consulta de scoring en read-only y mírala correr 10 días sin contactar a nadie. Verás rápido si tus scores cuadran con la realidad de tus firmas. Solo entonces conectas la alerta de Slack y ajustas los pesos con tus conversiones reales. Un scoring de intención útil cabe en una tabla de eventos, una consulta SQL y un cron — el CRM elefante, guárdatelo para cuando tengas un equipo comercial que alimentar.
La newsletter
Al suscribirte aceptas recibir la newsletter de Zylior. Baja en 1 clic en cada correo.