traffic

Unique Visitors vs Pageviews: The Only Metric Definition You Need

Why deduplicated visitors are the honest currency of a promotional platform, how a day-salt visitor hash works, and why it stops gaming.

Ask ten analytics vendors how they count a visitor and you will get eleven answers. When you are running a promotional platform, that ambiguity is not an academic problem - it directly determines what your members are paid for.

The four competing definitions

Pageviews. Every request for a page. One person refreshing a dashboard five times produces five. Useful for content sites measuring consumption depth; useless as a unit of exchange.

Sessions. A visit that ends after thirty minutes of inactivity. Better than pageviews, but a single person on three devices is three sessions, and an automated crawler that respects cookies is one session per run.

Unique visitors (cookie-based). Deduplicated by a cookie identifier. Reasonable, except that privacy-conscious browsers and script blockers silently inflate your numbers by resetting or omitting the identifier.

Unique visitors (server-side, day-salted). Deduplicated by a hash of network and client signals plus a rotating salt, computed at the edge and stored with a uniqueness constraint. Imperfect, but the failure modes are not exploitable at scale.

For a credit economy, only the last one survives scrutiny.

Why the day salt matters

Consider a stable hash of (ip, user_agent) with no salt. Two consequences follow immediately.

First, the hash is a persistent identifier. Anyone who can read your database and who knows the IP space of a network can potentially recover who visited what. That is a privacy liability you do not need.

Second, the deduplication window is effectively infinite. A user who visits in January and again in June counts once, which is wrong - they are two distinct visits and should be credited as two.

Rotating the salt daily fixes both:


CREATE TABLE promo_visits (
  promo_link_id INTEGER NOT NULL,
  visitor_hash  TEXT    NOT NULL,
  day           TEXT    NOT NULL,
  credited      INTEGER NOT NULL DEFAULT 0,
  UNIQUE(promo_link_id, visitor_hash, day)
);

Yesterday's hashes are now unlinkable to today's, so the identifier is ephemeral. Duplicate protection is precise to the day, which is the right granularity for a daily credit unit.

The constraint does the work, not the code

The important detail above is UNIQUE. Credit allocation should never be implemented as "check whether this visitor exists, then insert". Between the check and the insert there is a window in which a fast double-submit - or thirty parallel tab openings - becomes thirty credits.

Let the database refuse the duplicate:


const res = await env.DB.prepare(
  `INSERT OR IGNORE INTO promo_visits
     (promo_link_id, user_id, website_id, visitor_hash, day, credited)
   VALUES (?, ?, ?, ?, ?, 1)`
).bind(linkId, userId, websiteId, visitorHash, day).run();

const credited = res.meta.changes > 0;   // exactly one caller ever sees true

meta.changes is one for the first writer and zero for every other. No transaction needed, no race possible.

What this deliberately does not catch

Honesty about limitations is more useful than a security claim that does not hold.

  • Shared networks. A university or corporate NAT has thousands of people behind one address. On a day where many of them visit, you undercount. The salt means the damage is bounded to a single day.
  • Mobile carrier rotation. Some carriers rotate addresses mid-session within a small pool. A determined visitor can be counted more than once. In practice the effort to do this at scale exceeds the credit value.
  • Distributed automation. A botnet with residential exit nodes defeats client-signal hashing. This is the one attack that genuinely cannot be solved by counting. It has to be solved economically, by keeping the reward per credit below the cost of running the botnet.
Counting rules defend against honest people and lazy automation. Nothing defends against a funded adversary, and any platform that claims otherwise is selling something.

The economics that make it hold up

Say a credit represents one ten-thousandth of a dollar of attention. A residential proxy costs roughly a thousandth of a dollar per request. To farm a thousand credits - a dollar of value - an attacker spends about a dollar on proxies and a few hours of engineering.

The margin is thin enough that farming is not a business. That is the actual defence, and it is why keeping the credit value deliberately modest is a design decision rather than a limitation.

What a member should see

Transparency is the cheapest trust mechanism available. On TopWapi, every member dashboard shows four numbers with their definition attached:

MetricWhat it counts
Promo link viewsRaw loads of your promotional page
Unique visitorsDay-deduplicated humans who loaded it
Credits earnedOne per unique visitor, after the 8-second dwell check
Outbound clicksDay-deduplicated clicks from your toplist listing to your site

The gap between the first two is the most informative number on the page. A large gap means people are arriving and being told they have already been counted - usually because a campaign is circulating inside one network, which is worth knowing.

The one-line version

Count people, not requests. Deduplicate at the database, not in application code. Rotate the identifier daily so yesterday's data cannot be joined to today's. Keep rewards small enough that cheating costs more than it earns.

Everything else is implementation detail.

Continue reading