Scorer l'intention d'achat de tes visiteurs sans CRM lourd
Tu veux savoir qui parmi tes visiteurs est vraiment sur le point d'acheter — sans installer un CRM à 200 €/mois et passer trois jours à câbler des workflows. Bonne nouvelle : un simple pixel first-party (un script JS que tu poses sur ton site) suffit à capter les signaux qui comptent. Le reste, c'est de l'arithmétique pondérée que tu fais tourner en SQL ou dans un cron.
Les signaux qui valent quelque chose (et ceux que tu peux jeter)
L'erreur classique : tracker 40 événements et noyer le signal. En réalité, l'intention d'achat se lit dans une poignée de comportements. Voici ceux qui corrèlent réellement avec une conversion, par ordre de poids décroissant.
- Visite de la page pricing : le signal n°1. Quelqu'un qui ouvre `/pricing` se projette déjà en client. Pondère-le fort.
- Retour multi-session : 3 sessions sur 7 jours = curiosité qui devient besoin. Une seule session, même longue, vaut moins que trois courtes espacées.
- Temps actif (pas temps de page) : mesure le temps engagé (onglet visible + scroll/clic), pas l'onglet laissé ouvert pendant le déjeuner.
- Profondeur de scroll sur les pages clés : 80 % de scroll sur une page produit ou un cas client > 100 % de scroll sur le blog.
- Pages 'bottom-funnel' vues : docs d'intégration, page sécurité, comparatif vs concurrent, FAQ facturation. Ce sont les questions d'un acheteur, pas d'un curieux.
- Faux signaux à ignorer : un seul pageview home, le trafic référé depuis un agrégateur, les bots (filtre par user-agent et par sessions <2s).
Le pixel first-party : ce que tu poses, ce que tu envoies
First-party = ton propre endpoint, ton propre cookie. Pas de dépendance à un SaaS tiers, pas de blocage par les adblockers de tiers, et tu restes maître de la donnée (RGPD-friendly si tu restes en intention anonyme + consentement). Concrètement, un script léger qui envoie un battement de cœur (heartbeat) quand l'onglet est visible, et un événement à chaque page clé.
// 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 });
Côté serveur, `/collect` fait un `INSERT` brut dans une table `events(vid, type, path, ts, meta_jsonb)`. C'est tout. Tu ne calcules rien à la volée : tu stockes des événements bruts et tu scores en batch. Ça t'évite des bugs de double-comptage et tu peux re-scorer tout l'historique quand tu changes la pondération.
La formule de scoring : pondère, plafonne, décote
Le score d'un visiteur = somme de points par signal, plafonnée par signal (sinon un seul fou de scroll te fausse tout) et décotée dans le temps (une visite pricing d'il y a 20 jours ne vaut plus une visite d'hier). Voici une grille de départ — calibre-la ensuite sur tes vrais convertis.
- Visite `/pricing` : +25 par visite, plafond 50.
- Page bottom-funnel (docs, sécurité, comparatif) : +15 chacune, plafond 45.
- Session distincte (>30 min d'écart) : +10, plafond 40 (= 4 sessions).
- Tranche de 2 min de temps actif : +5, plafond 30.
- Scroll ≥75 % sur page produit/cas : +8, plafond 16.
- Décote temporelle : multiplie le score de chaque événement par `0.9 ^ (jours_écoulés)`. Un événement de 7 jours pèse ~48 % de sa valeur initiale.
-- 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;
Le seuil 'lead chaud' et le bon moment pour relancer
Un seuil absolu ("80 = chaud") vieillit mal. Préfère un seuil relatif : le top 10 % des scores du mois glissant. Ça s'auto-calibre quand ton trafic change. Concrètement, calcule le 90e percentile des scores actifs et traite tout ce qui dépasse comme un lead chaud à traiter dans les 24 h.
- Trop tôt : relancer dès la première visite pricing = tu cames quelqu'un qui faisait juste du benchmark. Attends un deuxième signal fort (retour OU page bottom-funnel).
- Fenêtre idéale : score franchit le 90e percentile ET dernier événement < 48 h. L'intention est mûre et la mémoire de ta marque est fraîche.
- Trop tard : score élevé mais dernier événement > 10 jours = train manqué. Relance quand même, mais avec un angle 'on a sorti X depuis', pas 'prêt à acheter ?'.
- Canal : score chaud + email connu (newsletter, essai) = email perso sous 24 h. Score chaud anonyme = retargeting léger ou bandeau in-app, pas de relance directe possible.
Commence minimal : pose le pixel cette semaine, stocke les événements bruts, lance la requête de scoring en read-only et regarde-la tourner 10 jours sans rien relancer. Tu verras vite si tes scores collent à la réalité de tes signatures. Ensuite seulement tu branches l'alerte Slack et tu ajustes les poids sur tes vrais convertis. Un scoring d'intention utile tient en une table d'événements, une requête SQL et un cron — le CRM usine à gaz, tu te le gardes pour quand tu auras une équipe commerciale à nourrir.
La newsletter
En t’inscrivant, tu acceptes de recevoir la newsletter Zylior. Désinscription en 1 clic dans chaque email.