> ## 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.

# Risk Scoring

> Learn how the Risk Score is built from 0 to 100, what its bands mean, and how each signal is weighted.

One integer from 0 to 100 summarizes how anonymous a visit looks, and the `signals` array next to it names every signal that produced that number. Higher means more anonymous: more likely masked, spoofed, or abusive. ShieldLabs surfaces the score and the reasons. Your own code decides allow, challenge, review, or block.

## What the Risk Score is

The Risk Score is one value per visit, with the evidence attached.

* **Range 0 to 100, hard-capped at 100.** There is no value above 100 and no negative score.
* **Explainable.** Every Risk Score ships with a `signals` array, so you never act on a black box.
* **Delivered, not computed in the browser.** It arrives by [webhook](/api/webhooks) and is readable from the [History API](/api/server-api).

Individual signals can sum past 100 internally, but the score you receive is always clamped to 100.

## How the score is built

Each signal that fires contributes a fixed number of points, and the points are summed. The `signals` array carries one entry per signal: `{ "name": "<name>", "weight": <int> }`. `weight` is the points that signal added. `signal` names it in plain language.

A realistic webhook body for a visit on a proxy with a timezone mismatch:

```json theme={null}
{
  "request_id": "13f84f05-3b2a-4f1e-9c7d-2a4b6e8f0a11",
  "device_id": "5eb7fd5c-8c2e-4a91-b0f3-1d7c9e2a4b55",
  "visitor_id": "161dfbad-2f4a-4c81-9e0b-7a3c5d8f1e22",
  "public_ip": {
    "ip": "203.0.113.42",
    "country": "DE"
  },
  "os": "Windows",
  "user_hid": "u_7f3c9a2b",
  "risk_score": 20,
    "signals": [
    { "name": "Proxy", "weight": 10 },
    { "name": "Timezone Mismatch", "weight": 10 }
  ],
  "observed_at": "2026-06-16T18:00:45Z"
}
```

The score is `20` because the two signals add 10 points each. Because both reasons are visible, your code can treat that 20 at a withdrawal differently from a 20 on a low-stakes action.

<Note>
  The `signal` string is a display label and can change — **never branch on it**. Identify which signals fired with the stable `detection_flags` booleans (and the Risk Score band). A numeric `weight` alone does not tell you which signal fired: 30 from `os_not_detected` is not the same case as 30 from `browser_vpn_proxy`. Read the band, the flags, and the sensitivity of the action together — see [Acting on the Risk Score](/guides/acting-on-risk-score).
</Note>

<Note>
  The Risk Score is delivered by [Webhooks](/setup/webhooks) and readable from the [Server API](/api/server-api). The browser does not compute it.
</Note>

## The weights

Every scoring signal carries a fixed weight, published below. This is the scoring contract: the same signal always adds the same points before the total is capped. The heavier the weight, the stronger and rarer the tell: Tor or a disabled-scripting client is near-certain masking, while a lone VPN or proxy is common and often legitimate, so it carries far less.

| Signal              | Weight | What it means                                                                                               |
| ------------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| Tor                 | 99     | Connection exits through the Tor network                                                                    |
| JavaScript Disabled | 90     | The page ran without the scripting a real browser provides, which points to automation or a headless client |
| OS Mismatch         | 60     | The operating system the browser reports is inconsistent with other evidence about the device               |
| Anti-detect Browser | 60     | Anti-detect or fingerprint-spoofing indicators are present                                                  |
| Browser Automation  | 60     | The browser is driven by an automation framework — the standard `navigator.webdriver` flag is set           |
| Stun not checked    | 30     | The follow-up network check did not complete                                                                |
| OS not Detected     | 30     | The OS could not be derived from the available signals                                                      |
| Browser VPN/Proxy   | 30     | An in-browser VPN or proxy extension was detected                                                           |
| VPN                 | 15     | Connection through a VPN, corroborated across multiple signals                                              |
| Privacy Relay       | 15     | iCloud Private Relay or a similar relay                                                                     |
| Proxy               | 10     | The IP is flagged as a proxy                                                                                |
| Datacenter IP       | 10     | The IP belongs to a datacenter or hosting range                                                             |
| Abuser Flag         | 10     | The IP or device is on an abuse reputation list                                                             |
| Timezone Mismatch   | 10     | The device timezone does not match the IP geolocation                                                       |

<Info>
  The names in this table are the human-readable labels shown in the dashboard, and [Anonymity Signals](/features/anonymity-signals) covers what each one means. The raw `signal` string on the wire can change — **never branch on it**. Use `detection_flags` to see which signals fired; use the score band for severity. The same weight from two different signals is not interchangeable — decide with flags plus context, not the number alone.
</Info>

## How signals combine

The score is additive, but a few rules shape which signals are added before the sum.

* **The strongest anonymity signal takes priority.** Tor, Privacy Relay, and VPN are evaluated in that order. If one fires, the weaker anonymity signals are not added on top of it.
* **A disabled-scripting visit yields almost no browser signals.** With nothing to collect in the page, the JavaScript Disabled weight dominates the score.
* **Anti-detect indicators do not stack.** One verdict is enough.
* **Proxy-family signals stack.** Proxy, Datacenter, and Abuser reputation signals add up with each other.

VPN is asserted only when multiple independent signals agree, which catches masking that a simple IP blocklist misses.

## How to use the score

The score maps to four bands, and these labels are the only ones ShieldLabs uses. The recommended action is **a guide, not a rule**. You set the rules: pick the action per band that fits your own risk tolerance.

| Band       | Range  | Meaning                                             | Recommended action (a guide, not a rule)  |
| ---------- | ------ | --------------------------------------------------- | ----------------------------------------- |
| **Clean**  | 0–9    | No meaningful signals                               | Pass through, no friction                 |
| **Low**    | 10–29  | One minor signal                                    | Allow, worth logging                      |
| **Medium** | 30–59  | Several overlapping signals, or one moderate signal | Step-up challenge, second look, or review |
| **High**   | 60–100 | Strong anonymity signals                            | Block, review, or require verification    |

ShieldLabs gives you the number and the reasons. Your code owns the verdict.

## Caveats

<Warning>
  A legitimate user can score high. A corporate proxy, a VPN, or a privacy browser all raise the score without any wrongdoing. Decide on **Risk Score plus signals plus context** (signup, login, payment, withdrawal), never on the number alone. Tune your thresholds gradually as you observe real traffic.
</Warning>

The risk scale is **0 to 100**. The value **999** is a rate-limit ban marker, not a 0–100 score and not a band; a rate-limit ban also surfaces as [HTTP 429](/rate-limits) at the gateway. The 999 marker can still arrive in the `score` field of a webhook or stored snapshot, so guard `score > 100` before you read the band.

## Next steps

<CardGroup cols={3}>
  <Card title="Acting on the Risk Score" icon="code" href="/guides/acting-on-risk-score">
    Turn the Risk Score plus signals into allow, challenge, review, or block in your own code.
  </Card>

  <Card title="Anonymity Signals" icon="list-check" href="/features/anonymity-signals">
    What each scoring signal covers and how it is surfaced.
  </Card>

  <Card title="Accuracy" icon="bullseye" href="/features/accuracy">
    Why corroborating independent signals reaches up to 99% accuracy.
  </Card>
</CardGroup>
