> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shieldlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Traffic Quality: rank sources by anonymous-traffic share

> Learn how to measure traffic quality: rank acquisition sources by anonymous-visit share to find your true cost per real visitor.

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)](/features/risk-scoring) 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](/features/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.

<Note>
  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.
</Note>

## 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 analytics     | ShieldLabs                                        |
| ----------------------------------------- | ---------------------- | ------------------------------------------------- |
| Unit counted                              | Sessions, pageviews    | Identified requests + Risk Score                  |
| "10,000 visits" means                     | 10,000 equal sessions  | A distribution across Clean / Low / Medium / High |
| Sees VPN, proxy, Tor, anti-detect routing | No                     | Yes, as named signals in `signals`                |
| Returning visitor after cleared cookies   | Counted as new         | Recognized by DeviceID (same browser)             |
| Per-source verdict                        | Volume and conversions | Volume, 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.

| Bucket         | Bands                    | What is usually in it                                              |
| -------------- | ------------------------ | ------------------------------------------------------------------ |
| **Clean**      | Clean (0-9), Low (10-29) | Direct connections, ordinary browsers, the bulk of healthy traffic |
| **Suspicious** | Medium (30-59)           | VPN, proxy, privacy relay, datacenter IP, timezone mismatch        |
| **Dangerous**  | High (60-100)            | Tor, anti-detect browsers, OS mismatch                             |

The split is driven by the named [anonymity signals](/features/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`](/glossary#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](/features/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.

<Steps>
  <Step title="Capture the source on every visit">
    Add the [snippet](/setup/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](/setup/csp) page lists the header requirements.

    ```html Landing page that paid and organic traffic arrives on theme={null}
    <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>
    ```
  </Step>

  <Step title="Read the Traffic Score card">
    On the [Overview](/features/traffic-analytics) 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.
  </Step>

  <Step title="Rank sources by risk">
    Open [Traffic Sources](/features/traffic-analytics). 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.
  </Step>

  <Step title="Export the raw records for BI">
    Go to the [Data](/features/traffic-analytics) 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 attribution** — `channel`, `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.
  </Step>

  <Step title="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.

    ```js theme={null}
    // 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](/use-case/affiliate-fraud).
  </Step>
</Steps>

<Tip>
  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](/features/patterns).
</Tip>

### 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](/api/server-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.

```bash theme={null}
# 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"
```

```json theme={null}
{
  "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
}
```

<Note>
  Parse `score_details` as JSON. Branch on `score` and each entry's `Value`, not on the human-readable `Description` label, which can change.
</Note>

If you would rather build the report in real time as traffic arrives, consume the [webhook](/setup/webhooks) 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:

<Steps>
  <Step title="Load a page in a normal window">
    Visit a page with the snippet installed and read the webhook (or the [History API](/api/server-api)). Note the `device_id`.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

<Note>
  The dashboard tooltip calls these **estimated** unique visitors, so do not promise exact counts. The [identifiers reference](/features/identification) has the full mechanics, and the [Visitors view](/features/traffic-analytics) shows how "new" is determined.
</Note>

## Where to go next

If you have not installed the snippet and a webhook yet, start with the [Quickstart](/quickstart). To understand the score and the named anonymity signals behind the split, read [Risk Score](/features/risk-scoring) and [Signals](/features/anonymity-signals). Per-source payout decisions build on this in [Affiliate Fraud](/use-case/affiliate-fraud), and the [Billing](/billing) page covers the per-request model.
