Skip to main content
Standard analytics measures volume: how many sessions, pageviews, and “new” users. It cannot tell you how much of that traffic is masked, spoofed, or coordinated. ShieldLabs attaches an explainable Risk Score (0-100) and named anonymity signals to every visit, so a noisy number like “10,000 visits” splits into traffic you can trust and traffic you cannot.

What is traffic quality?

Traffic quality is how much of an acquisition source’s traffic comes from real, unmasked visitors versus sessions that are anonymized, spoofed, or coordinated. A source can look healthy on volume alone while most of its clicks arrive over VPNs, proxies, datacenter IPs, or anti-detect browsers. ShieldLabs grades it by attaching a Risk Score and named anonymity signals to every visit, then rolling those up per channel, referrer, and UTM. The count stays honest because every visit resolves to a durable, server-derived DeviceID. It is computed from the browser environment rather than stored in a cookie, so it survives a cookie clear, an incognito window, and an IP rotation — one machine cannot churn cookies to pose as many “visitors” and inflate a source’s volume.
The Risk Score is 0-100, hard-capped, in four bands: Clean (0-9), Low (10-29), Medium (30-59), High (60-100). A higher score means more anonymous or masked traffic, not a confirmed verdict — a legitimate visitor can score high behind a corporate proxy, VPN, or privacy browser. For traffic-quality reporting you read the shape of the distribution across many requests, where individual false positives wash out.

Volume vs quality, in one number

A dashboard that only reports volume treats every session as equal. ShieldLabs adds a per-request risk dimension, so the same 10,000 visits become a quality breakdown you can act on.
Standard analyticsShieldLabs
Unit countedSessions, pageviewsIdentified requests + Risk Score
”10,000 visits” means10,000 equal sessionsA distribution across Clean / Low / Medium / High
Sees VPN, proxy, Tor, anti-detect routingNoYes, as named signals in signals
Returning visitor after cleared cookiesCounted as newRecognized by DeviceID (same browser)
Per-source verdictVolume and conversionsVolume, conversions, and risk share
A campaign sending 95% Clean traffic and one sending 40% High-band traffic can report identical visit counts in Google Analytics or Vercel Analytics. The Risk Score is what tells them apart. For quick reporting it helps to collapse the four bands into three plain buckets. This is a reporting convention, not a product feature — the bands stay Clean / Low / Medium / High everywhere in the product.
BucketBandsWhat is usually in it
CleanClean (0-9), Low (10-29)Direct connections, ordinary browsers, the bulk of healthy traffic
SuspiciousMedium (30-59)VPN, proxy, privacy relay, datacenter IP, timezone mismatch
DangerousHigh (60-100)Tor, anti-detect browsers, OS mismatch
The split is driven by the named anonymity signals: VPN, Proxy, Tor, Privacy Relay, Datacenter IP, Abuser Flag, Anti-detect Browser, OS Mismatch, Timezone Mismatch. Each arrives in the webhook signals array with the points it added, and as a boolean in detection_flags. One comparison earns special attention for reporting. Each scored visit carries two IP objects: public_ip is the public IP and country on the HTTP request, which a VPN or proxy lets a visitor fake; local_ip is the real network IP recovered from an optional follow-up check, which can reveal the network behind the mask. When the two disagree, detection_flags.ip_mismatch is set to true. A source whose visits routinely show public_ip.country differing from local_ip.country is sending masked traffic, no matter how clean the public IP looks.

Grade and act on each source

The whole workflow is three plain steps. ShieldLabs gives you the inputs; the grading rule and the budget call are yours.
  1. Read the anonymous-traffic share per source. For each channel, referrer, and UTM source, read the percentage of its requests in the Medium and High bands. That share is the source’s anonymity grade.
  2. Rank and flag sources by that share. Sort worst-first. Flag any source whose share crosses a line you set; a low share marks a source you trust as-is.
  3. Turn the grade into a budget decision. Divide each source’s spend by its real-visitor count, not its raw click count, to get cost per real visitor. Cut or renegotiate the sources paying click prices for masked traffic, keep the ones that look pricey per click but are mostly Clean, and report real-visitor numbers instead of raw volume.
A few cases worth flagging while you grade:
  • A channel or campaign with a high Dangerous share is the first place to cut or renegotiate, especially affiliate and referral sources.
  • A source that looks expensive per click but is mostly Clean may be your best traffic once reweighted to cost per real visitor.
  • Rising Medium and High share over time on Direct or Organic Search is a cue to check the Patterns tab for coordinated activity.

Build it

Reading traffic quality is part dashboard, part export. The snippet feeds both; the first step is the only code on the page.
1

Capture the source on every visit

Add the snippet to the page that receives the traffic. It already collects channel, referrer, and UTM attribution for every request, so the Traffic Sources tables populate without extra work. Make sure your UTM parameters are on the inbound links — ShieldLabs records utm_source, utm_medium, utm_campaign, utm_content, utm_term, referrer_domain, and the resolved channel per request. The CSP page lists the header requirements.
Landing page that paid and organic traffic arrives on
<script type="module">
  const mod = await import('https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY');
  mod.checkAnonymous(undefined, (ip, requestID) => {
    // The callback returns the client IP and requestID. The Score is
    // not returned here; it arrives later by webhook.
    // Optional: stash the requestID to attribute a later conversion.
    document.cookie = `shield_rid=${requestID}; max-age=3600; SameSite=Lax`;
  });
</script>
2

Read the Traffic Score card

On the Overview tab, the Traffic Score card shows a gauge with the band label, a Traffic Risk metric (a 0-100 indicator weighting the concentration of Medium and High requests by signal severity, where lower is better), and Requests Checked. Below the gauge, a stacked bar shows the count and percent of requests in each band: Clean, Low, Medium, High. This is your quality split for all traffic at a glance. Filter by project and date range with the controls at the top.
3

Rank sources by risk

Open Traffic Sources. The Channels table lists Google Ads, Meta, TikTok, LinkedIn, X, Organic Search, Referral, Direct, and Other. Each row shows requests, traffic share, and a Risk Badge rendered as <score> <level> (for example 71 High or 8 Clean). Sort by Traffic Risk to find the channel sending the most masked traffic. The Source details table below toggles between Referrers and UTM Parameters, so you can isolate the single affiliate, creative, or campaign delivering anonymous traffic inside an otherwise healthy channel.
4

Export the raw records for BI

Go to the Data tab and use Export to pull the per-request records as CSV. Exports are free (they do not bill requests). The export is the one path that carries per-request source attributionchannel, referrer_domain, and the utm_* fields — alongside the Score and signal columns, so it is what you join against ad spend.For per-source roll-ups, the boolean detection_flags (vpn, proxy, tor, datacenter_ip, anti_detect_browser, ip_mismatch, suspicious_paid_click, and the rest) aggregate more cleanly than the variable-length signals array. SUM each flag grouped by channel or utm_* to get a masking-share breakdown per source.
5

Compute cost per real visitor

Group the exported rows by source and divide spend by the Clean-bucket count instead of the raw count. That stops you from paying click prices for masked traffic.
// One row per source for a reporting window. `spend` comes from your
// ad platform; `requests` and `bands` come from the ShieldLabs export.
const sources = [
  { source: "google_ads", spend: 4000, requests: 10000, bands: { clean: 8800, low: 700, medium: 350, high: 150 } },
  { source: "affiliate_x", spend: 3000, requests: 9000,  bands: { clean: 2200, low: 600, medium: 2400, high: 3800 } },
];

function trafficQuality({ source, spend, requests, bands }) {
  // Reporting convention: Clean bucket = Clean + Low bands.
  const realVisitors = bands.clean + bands.low;
  const dangerous = bands.high;

  return {
    source,
    requests,
    realVisitors,
    cleanShare: realVisitors / requests,
    dangerousShare: dangerous / requests,
    costPerClick: spend / requests,
    costPerRealVisitor: spend / Math.max(realVisitors, 1),
  };
}

for (const s of sources) {
  const q = trafficQuality(s);
  console.log(
    `${q.source}: $${q.costPerClick.toFixed(2)}/click -> ` +
    `$${q.costPerRealVisitor.toFixed(2)}/real visitor ` +
    `(${(q.cleanShare * 100).toFixed(0)}% clean, ` +
    `${(q.dangerousShare * 100).toFixed(0)}% dangerous)`
  );
}
// google_ads: $0.40/click -> $0.42/real visitor (95% clean, 2% dangerous)
// affiliate_x: $0.33/click -> $1.07/real visitor (31% clean, 42% dangerous)
On a cost-per-click basis affiliate_x looks cheaper. On a cost-per-real-visitor basis it is more than twice as expensive, because most of its traffic is masked. That is the reallocation decision standard analytics cannot surface. For paid sources specifically, the webhook exposes a ready-made suspicious_paid_click flag in detection_flags, true when a request arrived on a paid channel and scored High (60+), so you can sum it per utm_campaign or channel without re-deriving the channel-plus-score logic. Paying out conversions on top of this is covered in Affiliate Fraud.
The Traffic Score card counts in requests, while the Patterns Summary card on the same Overview tab counts abuse-pattern detections against unique visitors. They use different denominators on purpose, so do not expect their totals to reconcile. For “how risky is my traffic,” read Traffic Score; for “which identities cross abuse thresholds,” read Patterns.

Read records by identifier

The dashboard and export cover reporting. To read scored records by identifier in real time — to reconcile or enrich a row — use the History API. It returns newest-first snapshots with score, score_details, and network fields, but not the channel/UTM attribution, so join back to the export on request_id when you need source attribution. History reads through account.shieldlabs.ai are free.
# Snapshots for one visitor, newest first.
curl "https://account.shieldlabs.ai/api/v1/history/visitor_id/c4a2e9b1-5f8d-4c3a-8e7b-2a1f0d9c8b76?limit=50" \
  -H "Authorization: Bearer sec_your_private_api_key"
{
  "data": [
    {
      "request_id": "8f1d0c2a-7b3e-4a9c-9d2f-1e6a5b4c3d21",
      "visitor_id": "c4a2e9b1-5f8d-4c3a-8e7b-2a1f0d9c8b76",
      "device_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
      "ip": "203.0.113.42",
      "country": "US",
      "risk_score": 90,
      "score_details": "[{\"Value\":60,\"Description\":\"OS Mismatch\"},{\"Value\":30,\"Description\":\"Browser VPN/Proxy\"}]",
      "connection_type": "proxy",
      "browser": "Chrome",
      "device_type": "desktop",
      "created_at": "2026-06-16 10:00:21"
    }
  ],
  "total": 1
}
Parse score_details as JSON. Branch on score and each entry’s Value, not on the human-readable Description label, which can change.
If you would rather build the report in real time as traffic arrives, consume the webhook and aggregate the score per source in your own store. Webhooks are at-most-once with no retries, so make the handler idempotent on request_id and reconcile against the History API for guaranteed completeness.

Test it

Confirm the DeviceID holds before you trust the quality split:
1

Load a page in a normal window

Visit a page with the snippet installed and read the webhook (or the History API). Note the device_id.
2

Repeat in incognito and after clearing cookies

Open the same page in a private window, then again after clearing cookies and storage. The cookie_id and visitor_id change, but the device_id stays the same — that is what keeps a returning visitor from counting as new.
3

Re-test over a VPN

Reconnect through a VPN or proxy and reload. The IP changes and a masking signal such as VPN or Proxy appears in signals, while the device_id is unchanged. That is one identity over masked traffic, exactly the case the quality report is built to surface.
The dashboard tooltip calls these estimated unique visitors, so do not promise exact counts. The identifiers reference has the full mechanics, and the Visitors view shows how “new” is determined.

Where to go next

If you have not installed the snippet and a webhook yet, start with the Quickstart. To understand the score and the named anonymity signals behind the split, read Risk Score and Signals. Per-source payout decisions build on this in Affiliate Fraud, and the Billing page covers the per-request model.