zylior
← Blog

Score your visitors' purchase intent without a heavy CRM

You want to know which of your visitors is really about to buy — without installing a $200/month CRM and spending three days wiring up workflows. Good news: a simple first-party pixel (a JS script you drop on your site) is enough to capture the signals that matter. The rest is just weighted arithmetic you run in SQL or in a cron job.

The signals that are worth something (and the ones you can throw away)

The classic mistake: tracking 40 events and drowning the signal. In reality, purchase intent shows up in a handful of behaviors. Here are the ones that actually correlate with a conversion, in decreasing order of weight.

The first-party pixel: what you drop in, what you send

First-party = your own endpoint, your own cookie. No dependency on a third-party SaaS, no blocking by third-party adblockers, and you stay in control of the data (GDPR-friendly if you stay in anonymous intent + consent). Concretely, a lightweight script that sends a heartbeat when the tab is visible, and an event on each key page.

// 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 });

On the server side, `/collect` does a raw `INSERT` into an `events(vid, type, path, ts, meta_jsonb)` table. That's it. You compute nothing on the fly: you store raw events and you score in batch. That saves you double-counting bugs and lets you re-score the whole history whenever you change the weighting.

The scoring formula: weight, cap, decay

A visitor's score = sum of points per signal, capped per signal (otherwise a single scroll maniac skews everything) and decayed over time (a pricing visit from 20 days ago is no longer worth a visit from yesterday). Here's a starting grid — then calibrate it on your real converters.

-- 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;
Don't guess your weights. Export your last 20 signed customers, replay their signal history before signing, and look at the scores they had. If your converters maxed out at 40 and your 'hot' threshold is at 80, your threshold is wrong — not your customers.

The 'hot lead' threshold and the right moment to follow up

An absolute threshold ("80 = hot") ages badly. Prefer a relative threshold: the top 10% of scores over the rolling month. It auto-calibrates when your traffic changes. Concretely, compute the 90th percentile of active scores and treat anything above it as a hot lead to handle within 24h.

Wire up a single alert: an hourly cron that pushes into a `#hot-leads` Slack channel every `vid` that just crossed the 90th percentile, along with its top-3 viewed pages. You handle it by hand at first — 5 leads/week justify no automation. You only automate follow-up once volume overwhelms you.

Start minimal: drop the pixel this week, store the raw events, run the scoring query read-only and watch it run for 10 days without following up on anything. You'll quickly see whether your scores match the reality of your signings. Only then do you wire up the Slack alert and tune the weights on your real converters. A useful intent scoring fits in one events table, one SQL query and a cron — the bloated CRM, save that for when you have a sales team to feed.

The newsletter

By subscribing you agree to receive the Zylior newsletter. One-click unsubscribe in every email.