> Source: https://kaidn.io/docs/concepts
> Full documentation index: https://kaidn.io/llms.txt

Docs menuAll pages, quickstarts and this page’s contentsDocs
- [Introduction](https://kaidn.io/docs)
- [Quickstarts](https://kaidn.io/docs/quickstart)
- [Core concepts](https://kaidn.io/docs/concepts)
- [API reference](https://kaidn.io/docs/api)
- [Guides](https://kaidn.io/docs/guides)
- [Keys & dashboard](https://kaidn.io/docs/keys)
- [Glossary](https://kaidn.io/glossary)

Web
- [JavaScript](https://kaidn.io/docs/quickstart/javascript)
- [React](https://kaidn.io/docs/quickstart/react)
- [Next.js](https://kaidn.io/docs/quickstart/nextjs)
- [Preact](https://kaidn.io/docs/quickstart/preact)
- [Vue](https://kaidn.io/docs/quickstart/vue)
- [Nuxt](https://kaidn.io/docs/quickstart/nuxt)
- [Angular](https://kaidn.io/docs/quickstart/angular)
- [Svelte](https://kaidn.io/docs/quickstart/svelte)

Mobile
- Androidsoon
- iOSsoon
- React Nativesoon
- Fluttersoon

Server
- [Node.js](https://kaidn.io/docs#quickstart)
- [PHP](https://kaidn.io/docs/quickstart/php)
- C#/.NETsoon
- Gosoon
- Javasoon
- [Python](https://kaidn.io/docs/quickstart/python)

On this page
- [How a verdict is made](#verdict)
- [The four signal families](#signals)
- [Recognising a device](#identity)
- [The connection fingerprint](#fingerprint)
- [The shared fraud graph](#graph)
- [Where this fails](#limits)

# Core concepts

Six ideas. Read them once and every field in the [reference](https://kaidn.io/docs/api) will make sense without looking anything up.

Terms are introduced here rather than assumed. Anything still unfamiliar is defined in the [glossary](https://kaidn.io/glossary).

## How a verdict is made

Rules, not a model. Every check that fires contributes a weight, the weights sum into a score from 0 to 100, and the score crosses a threshold into one of three verdicts.

| verdict | default range | what it means |
| --- | --- | --- |
| `allow` | under 40 | nothing worth acting on. Let it through |
| `review` | 40 to 69 | worth a human look before you grant something valuable |
| `block` | 70 or more | act on it. Deny, hold, or withhold the reward |

**Both thresholds are yours to move**, per account, and so is every weight. A rewards operator paying real money at signup will run tighter than a content site protecting a comment box.

Two design decisions follow from choosing rules over a model, and they are the reason the product looks the way it does. **You can audit a sum.** Every verdict carries the checks that fired, their weights, and the raw evidence behind each, so a decision can be explained to the person it was made about. **And you can change a weight yourself** without asking anyone, because there is nothing to retrain.

An LLM writes the plain-English `reason_text` that comes back with the verdict. It never decides the outcome, and if it is unavailable you get a templated sentence instead. A score is never blocked on a language model.

**Most signals cannot reach `block` alone.** That is deliberate, not timidity. One fact about a stranger is rarely enough to convict them, so the weights are set so that a single signal reviews and two agreeing signals block. If you want a hard single-signal block, that is what your own blocklist is for.

## The four signal families

Everything the engine knows falls into four groups. The [reference lists every code](https://kaidn.io/docs/api#reasons) with its weight; this is what each family is actually for.

#### 1. Where the request came from

Whether the address is a home connection, a phone, a data centre, or a proxy someone is renting. A signup from a hosting provider is usually a script. The complication is that a clean residential address now costs pennies, so this family alone stopped being sufficient years ago.

It works at the level of the **network** rather than the single address. Every address belongs to a network run by somebody, identified by a number: an [ASN](https://kaidn.io/glossary#asn). Flagging the ASN catches an entire proxy seller instead of chasing addresses one at a time, and it is also the thing that separates two strangers who share a device fingerprint, which matters below.

#### 2. Whether the browser is what it claims

Automation tells, a software or virtual-machine graphics driver, a fingerprinting API that has been patched, an operating system claim contradicted by another signal. These catch the tools built specifically to defeat detection. Each one is high-precision and still contributing rather than convicting, because a rare privacy setup can trip any of them individually.

#### 3. Reuse and speed

How many accounts share one identity, and how fast things are happening. One signup tells you almost nothing. The same signup twelve times in a minute tells you a great deal. The industry word for the speed half is [velocity](https://kaidn.io/glossary#velocity), and the default window is ten minutes.

**Mobile networks put thousands of unrelated people behind one address.** So a velocity threshold that looks obviously right on desktop traffic will punish geography on mobile traffic. Set these against your own numbers, not against intuition.

#### 4. Email identity

Whether the address can receive mail at all, whether the domain exists to be thrown away, and whether it is a re-spelling of an inbox you already have. `bob+1@gmail.com`, `b.o.b@gmail.com` and `bob@googlemail.com`are one mailbox, and every scored event returns the canonical form so you can dedupe on it without encoding any provider's rules yourself.

## Device identity: the three rungs

A browser fingerprint is **not an identity**. It is an attribute hash, and attribute hashes collide: on our own production traffic **one iOS Safari fingerprint covers 2.30 different people**, because a default iPhone is genuinely identical to another default iPhone. Scoring that number as though it were a person is how fraud tools end up banning families.

So Kaidn does not score `device_id`. It resolves the event to the strongest identity it can actually defend, tells you *which* of three methods produced it, and attaches a measured probability that the identity still covers more than one person. You get the resolution and the confidence in the same response, and you can branch on either.

#### The ladder

| rung | how the identity is built | links across | collision risk |
| --- | --- | --- | --- |
| **1. Remembered**deterministic | You replayed a `device_token` we issued on an earlier visit and signed. No inference at all: we are reading back an id we minted. | everything. Survives a network change, a new IP, a cleared fingerprint, 400 days | **0.01**not zero: a household shares a browser |
| **2. Derived**probabilistic | No token, but we have both a fingerprint and the network it came from, so the identity is `h(device_id, asn)`. Derived, never stored: the same inputs always give the same id. | repeat visits on the same network | **0.01 to 0.21**by platform, see below |
| **3. Nothing**probabilistic | No token, and no fingerprint or no ASN to derive from. You get a fresh random id that **links nothing by construction**, rather than a shared key invented out of one weak input. | nothing. It is a one-off and says so | **0.03 to 0.57**reported for comparability only |

Read the rung off `device.resolution_rung`, the method off `device.resolution`, and the confidence off `device.collision_risk`. Without the cookie the ladder tops out at rung 2, which is where the large majority of events land; rung 3 is what is left when the beacon did not run or the ASN did not resolve.

#### Why the network is the second rung

Because we measured it rather than guessed. Of 132 fingerprints carrying more than one account (5.23 users each), splitting them by **ASN** resolved them down to 1.45 users per key. Splitting by JA4 fixed 1 of 132. Splitting by timezone fixed none. The thing that separates two strangers holding identical iPhones is not anything on the device, it is the network they are sitting on.

| platform | raw fingerprint | rung 2, h(id, asn) | risk before | risk after |
| --- | --- | --- | --- | --- |
| `iOS / Safari` | 2.30 users per key | 1.27 users per key | 0.57 | **0.21** |
| `Android / Chrome` | 1.25 | 1.05 | 0.20 | **0.05** |
| `iOS / Chrome` | 1.03 | 1.01 | 0.03 | **0.01** |
| all traffic | 1.53 | 1.14 | 0.35 | **0.12** |

Measured on production, not modelled. `device.risk_measured` tells you whether the figure you got was measured for that platform or is the all-traffic average standing in for it, which matters because the average is dominated by the two biggest platforms and says nothing about a rare in-app webview.

**The cost of rung 2, stated plainly:**it fragments. One phone moving from mobile data to home wifi becomes two identities, so a single person's accounts can scatter across keys. That trade is right for linkage, because a fragmented identity under-links and a colliding one accuses the wrong person, and it is exactly why rung 1 is strictly better wherever you can reach it. A cookie survives the network change that splits this key.

#### How to use each field

- **Link visits on `resolved_id`, never on `id`.** `id`is the raw hash, kept for debugging and for the dashboard's device view. It is the number that collides.
- **Gate your own automation on `collision_risk`.** A useful rule: act automatically below 0.05, require a human above it, and treat anything above 0.2 as a hint rather than evidence.
- **Trust `account_count_same_network` over `account_count`.** The first is the number you can defend in front of an angry user. The second includes collisions.
- **Watch `token_rejected`.** It is set when a `device_token` was presented but could not be used (`malformed`, `bad_signature`, `expired`). It is almost always a broken cookie integration rather than an attacker, and the event quietly falls back to the rung a first-time visitor gets.
- **Rung 3 is not a signal.** If you see it often, your beacon is not running or your ASN is not resolving. Do not read a random id as a new device.

#### Climbing to rung 1: the device token

Every `/v1/score` response carries a `device_token`. Store it as an ordinary first-party cookie on your own domain and send it back as `device_token`on that browser's next event. That is the whole mechanism. The token is signed, scoped to your tenant, meaningless to any other operator, re-signed on every call, and valid for 400 days. Because your server sets it on your own domain it is genuinely first-party, which is what buys the 400 days; script-written storage is capped at 7 days on Safari.

`@kaidn/sdk` does the cookie handling for you via `scoreWithCookie()`. Check `r.device.resolution === "deterministic"` to confirm it worked.

#### What the rung controls beyond your own code

Two things, and both are deliberate. **Scoring:** the `device_reuse` check uses the corroborated weights at rung 1 outright, because a replayed token needs no second opinion, and at rung 2 only once 3+ accounts agree on one network. **The cross-operator graph:** a device identity is only shared beyond your tenant when its collision risk is at or below `0.20` *and* that risk was measured for its platform. iOS Safari sits at 0.21 even after the network split, so it stays withheld. That is the correct outcome rather than a threshold to tune around: the platform that homogenises hardest is the one whose accusations travel least.

**Honest limits.** These figures come from one tenant, over one month, on traffic concentrated across four mobile carriers. They are published because a rough honest error bar beats a confident silent guess, and they will be re-measured as more operators send traffic. Confidence never changes a score on its own: it describes, and you decide.

## Device fingerprint: @kaidn/fp + JA4

Add this when you need to recognise the same person coming back. One script tag gives every visitor a stable `device_id`, and lets Kaidn capture their **JA4 TLS fingerprint** at the edge. JA4 comes from the TLS handshake rather than from JavaScript, so **the page cannot edit it**: it survives User-Agent swaps, incognito and IP rotation, and links accounts from one machine even when their `device_id` differs. **It is not unspoofable**, and anyone selling it that way is overselling: a client that builds its own TLS stack can choose what to present. It also identifies the *software*, not the person, so millions of ordinary Chrome users share one value. Treat it as a strong link between sessions, never as an identity on its own.

#### How it works

- Create a tracker in the dashboard (**Fraud Scoring API → Device trackers**) to get a publishable key locked to your domains. Drop its `` tag on your page and call `Kaidn.trigger('#your-form')`. On submit it fingerprints the device, beacons the JA4 to our edge, and appends a hidden `kaidn_device_id` field before the form submits.
- Your backend reads that field and calls `/v1/score` with the same `device_id`. Kaidn looks up the captured JA4 and folds it into the score. You don't forward it, and the verdict is returned to your server, never the browser.

**Metering:** the `/v1/fp` beacon is **free** and rate-limited. You are billed per `/v1/score` decision, fingerprint collection is included, never a separate line item.

The publishable key is safe to expose in page source, it can only send fingerprints (never read data or score) and is locked to the domains you set on the tracker, so a copied key can't be used from another site. The tag must run in the browser (a direct browser→edge request is what makes the JA4 the user's, not your server's). `Kaidn.store()` attaches your own IDs; `Kaidn.pause()` / `resume()` defer collection until late-entered data is available; `afterResult()` gives you the collection result (device_id, never a score). Fingerprint failures fail open, so a blocked script never blocks your signup. Adds the `headless_browser`, `device_inconsistent`, `emulated_environment`, and JA4-backed `seen_in_fraud_network` reason codes.

#### Anti-detect & VM detection

Anti-detect browsers (Multilogin, GoLogin, AdsPower) randomise the JS fingerprint per profile, so a stable `device_id` is impossible there by design. Instead we detect the *environment*via tells they can't hide: a software / VM GPU (SwiftShader, llvmpipe, VMware), farm-grade hardware, and a sparse font list. That sets `device.is_emulated`, and, paired with the server-side JA4, which those tools rarely spoof, survives per-profile randomisation. Fires the `emulated_environment` reason (contributing, not a hard block).

#### Session heartbeat, `Kaidn.watch()`

Call `Kaidn.watch()` on a logged-in / session page and Kaidn re-beacons the same `device_id` every ~60s (and on tab refocus). Because the `device_id` and JA4 stay constant across a VPN change, a beacon whose IP flips connection type mid-session is the tell: a dropped VPN leaking the real home IP (`vpn_drop`), a device that starts cloaking (`ip_cloaking`), or one device hopping across many IPs (`device_ip_hopping`). Returns `{ stop() }`.

```
<!-- Create a tracker in the dashboard (Fraud Scoring API → Device trackers)
     to get your publishable key + this exact snippet, locked to your domains. -->
<script src="https://api.kaidn.io/fp/pk_live_YOUR_KEY.js" defer></script>
<script>
  // The tag above is deferred, so it runs AFTER the page is parsed. This inline
  // block would otherwise run first and hit "Kaidn is not defined".
  document.addEventListener('DOMContentLoaded', function () {
    // Bind your form: on submit, Kaidn fingerprints the device, beacons the
    // JA4 to our edge, appends a hidden "kaidn_device_id" field, THEN submits.
    // Your backend receives device_id automatically, no manual wiring.
    Kaidn.store('user_id', window.CURRENT_USER_ID);  // optional: attach your IDs
    Kaidn.trigger('#signup-form');
    Kaidn.init();

    // Optional: on logged-in / session pages, start a heartbeat so Kaidn sees the
    // connection over time and catches a VPN drop (real IP leaking) mid-session.
    var watch = Kaidn.watch();     // re-beacons the same device_id every ~60s
    // ...later, on logout / route change:  watch.stop();
  });
</script>
```

## The shared fraud graph

The signals above only know what *you* have seen. The graph is how an operator benefits from what everybody else has seen: an entity confirmed as fraud on one site raises the score for the next one, so a ring that burns through one platform arrives at the next already known.

**It is opt-in and off by default**, in both directions. Two things then keep it honest.

- **Nothing readable is shared.** Entities travel as one-way [peppered hashes](https://kaidn.io/glossary#hmac). Two operators can discover they have both seen the same address without either of them, or us, being able to turn the value back into an address, and without an attacker confirming a guess by hashing it themselves.
- **A weak identity is not allowed to travel.** A device identity only crosses when its collision risk is at or below `0.20` *and*that risk was measured for its platform. iOS Safari sits at 0.21 even after the network is folded in, so iOS Safari devices are withheld entirely. An accusation that cannot be defended should not reach a stranger's customer.

You contribute by reporting real outcomes with [`POST /v1/label`](https://kaidn.io/docs/api#feedback). A confirmed fraud or chargeback only counts for an event you actually scored, so nobody can flag entities they never saw.

## Where this fails

Every vendor in this market has failure modes and most publish none of them. Ours, so you can plan around them rather than discover them.

- **A browser spoofing the operating system it already runs on defeats us.** We measured it against the market leader at the same time and it defeated them too. This is the current state of the art on both sides, and we would rather say so.
- **Large commercial residential proxies need a paid feed** we do not currently buy. We catch the ASN-level sellers and the datacenter ranges; a true rotating residential network is a known gap.
- **Anything running in the browser can be blocked.** The collector is a script, and a script can be stopped by an extension. We measured what that does to a competitor and the same architecture applies to us.
- **Our published rates come from one operator's traffic.** The collision figures on this page are real measurements rather than estimates, but they are one tenant over one month. They will move as more operators send traffic.

The full method and numbers: [FingerprintJS Pro and Kaidn, side by side in 24 privacy browser sessions](https://kaidn.io/blog/fingerprinting-blocked-agent).

[PreviousQuickstartsPick your stack and get a first score](https://kaidn.io/docs/quickstart)[NextAPI referenceEvery endpoint, field and reason code](https://kaidn.io/docs/api)
