Skip to content

Repository files navigation

KnowYourClient

Fraud detection, traffic segmentation, and identity tracking for any website.

KnowYourClient tells you who is really on your site: whether the browser is what it claims to be, whether a human is driving it, where the connection actually comes from, and whether this actor has been doing something suspicious across your traffic over time.

It was built to stop a specific problem — organized rings running card testing and spam against consumer brands, behind VPNs, residential proxies, bots, and anti-detect browsers — and it is designed so that any brand on any stack can deploy it.

const kyc = KYC.protect({ endpoint: '/kyc/assess' });

const verdict = await kyc.submit({ type: 'checkout', email, bin, amount });
if (verdict.action === 'deny') blockOrder(verdict.reasons);

Why this exists

Card testing is the attack that hurts most, and it is invisible to per-request checks. Every attempt in a carding run looks like an ordinary customer buying an ordinary cheap thing. Only the run gives it away — one device, many cards, a fresh IP each time. The damage is not just the fraud: it is the decline ratio, which puts your payment processing itself at risk.

So KnowYourClient is built around three ideas:

  1. Any single signal is spoofable, so conviction requires corroboration. Anti-detect browsers exist to make a browser report whatever you want. What they cannot do is keep dozens of independently-derived facts mutually consistent — the User-Agent, the platform string, Client Hints, the GPU driver string, the installed font set, codec support, the JS engine's error formatting, and the timezone reported through two different APIs. Contradictions between them are the signal.
  2. The client is hostile. Everything the browser reports is treated as evidence to be corroborated, never as fact. Signals are weighted by where they came from, and a payload that proves to be forged loses the benefit of everything inside it.
  3. False positives are the real failure mode. A fraud tool that blocks real customers costs more than the fraud it prevents. Roughly half the test suite exists to prove that screen-reader users, VPN users, in-app browser shoppers, and people on shared devices get through.

Try it in two minutes

git clone https://github.com/akhlas17/knowyourclient.git
cd knowyourclient
pnpm install && pnpm build

node examples/simulate-attack.mjs   # replays a carding campaign, prints what the engine sees
node examples/demo-server.mjs       # then open http://localhost:8787

The demo page runs the real collector against your actual browser and scores it with the real engine. Submit the checkout form a few times with different BINs and watch the velocity rules fire.


Install

npm install @knowyourclient/browser @knowyourclient/server

Or drop the bundle straight into a page — no build step, no npm:

<script src="/knowyourclient.collector.min.js"></script>
Bundle Size (gzip) Use it when
knowyourclient.collector.min.js 18 KB You have a KnowYourClient server. Put this on checkout.
knowyourclient.min.js 27 KB You want a local verdict with no server at all.
knowyourclient.esm.js 33 KB You are bundling it yourself.

dist/INTEGRITY.txt carries SRI hashes for each build, for PCI DSS 6.4.3 payment-page script inventories.


The two ways to run it

Client-only — visibility in five minutes

<script src="/knowyourclient.min.js"></script>
<script>
  KYC.assess().then((result) => {
    console.log(result.score, result.action, result.reasons);
    console.log('device:', result.identity.deviceId);
  });
</script>

This catches sloppy automation and obvious environment spoofing, which is a large share of volumetric abuse. Be clear about the limits, because the library is: a browser cannot see velocity, IP reputation, or what any other identity has been doing, and everything it reports about itself can be forged. confidence is capped at 0.5 to reflect that.

Client + server — what actually stops organized fraud

Browser:

<script src="/knowyourclient.collector.min.js"></script>
<script>
  // Start on page load. Behaviour has to be observed across the whole visit,
  // not sampled at submit time.
  const kyc = KYC.protect({
    endpoint: '/kyc/assess',
    onVerdict: (v) => console.log(v.score, v.action),
  });

  checkoutForm.addEventListener('submit', async (e) => {
    e.preventDefault();
    const verdict = await kyc.submit({
      type: 'checkout',
      email: emailInput.value,
      bin: cardNumber.slice(0, 6),      // first 6 digits only — never the full PAN
      cardToken: pspToken,              // opaque token from Stripe/Adyen/etc.
      amount: 89.99,
      currency: 'USD',
      billingCountry: 'US',
    });

    if (verdict && verdict.action === 'deny') return showError();
    if (verdict && verdict.action === 'challenge') return showCaptcha();
    e.target.submit();
  });
</script>

Server (Express):

import express from 'express';
import { Engine, expressAssess, expressNonce } from '@knowyourclient/server';

const engine = new Engine({
  salt: process.env.KYC_SALT,          // per-tenant secret; keep it stable
  policy: { shadowMode: true },        // start here — see below
});

const app = express();
app.get('/kyc/nonce', expressNonce(engine));
app.post('/kyc/assess', express.json(), expressAssess(engine, {
  trustedProxies: 1,                   // number of reverse proxies YOU run
  onAssessment: (a) => logger.info({ kyc: a }),
}));

Any other framework: use buildServerContext(req) and engine.assess({ payload, server, event }) directly. There is nothing Express-specific in the engine.


Start in shadow mode

const engine = new Engine({ salt, policy: { shadowMode: true } });

Shadow mode computes and reports everything but never escalates past monitor. Run it for a week, look at the score distribution on your traffic, then set thresholds at a percentile you are comfortable with.

The shipped thresholds are starting points, not truth. Every brand's baseline differs, and shipping thresholds without measuring is how fraud tools acquire a reputation for blocking customers.

const engine = new Engine({
  salt,
  policy: {
    bands: { low: 20, medium: 40, high: 65, critical: 85 },
    actions: { trusted: 'allow', low: 'allow', medium: 'monitor', high: 'challenge', critical: 'deny' },
    disabled: ['SPF_LANG_IP_MISMATCH'],   // turn off anything that misfires for you
    weights: { NET_KNOWN_VPN: 5 },        // or just retune it
  },
  velocityThresholds: { 'device:cards:1h': 2 },
});

What a verdict looks like

{
  "score": 100,
  "band": "critical",
  "action": "deny",
  "confidence": 0.75,
  "segments": ["payment:card-testing", "identity:disposable-email", "bot:no-interaction"],
  "identity": {
    "deviceId": "9f2a…",          // stable across browser updates, zoom, and DST
    "browserId": "4c81…",
    "sessionId": "1a9f…",
    "deviceConfidence": 1,
    "fuzzyMatched": false
  },
  "reasons": [
    { "code": "VEL_CARD_ATTEMPTS_DEVICE", "weight": 55, "category": "velocity",
      "detail": "One device attempted many distinct cards in a short window.",
      "evidence": { "key": "device:cards:1h", "value": 8, "threshold": 3 } },
    { "code": "VEL_CARD_ATTEMPTS_SUBNET", "weight": 40, "category": "velocity", "…": "" },
    { "code": "ID_DISPOSABLE_EMAIL",      "weight": 30, "category": "identity", "…": "" }
  ]
}

Every point traces to a named reason code. There is no opaque residual — which is what makes the system tunable, supportable, and defensible if a customer or a regulator asks why.

segments is the other half of the product: filter, route, and report on traffic by fraud pattern without ever touching the numeric score.


What it detects

Family Examples
Card testing Many cards per device/IP/subnet, decline streaks, BIN bursts, sequential PANs, minimal carts, sub-human checkout speed
Automation navigator.webdriver, automation-framework globals, CDP artifacts, headless builds, software rasterizers, crawler UAs
Anti-detect browsers UA vs platform vs Client Hints vs GPU vs fonts vs codecs vs error-stack contradictions, patched native functions, redefined navigator properties
Network Datacenter ASNs, Tor, open proxies, residential-proxy behaviour, impossible travel, ASN hopping
Behaviour No interaction, perfectly linear pointer paths, inhuman keystroke rhythm, scripted form fill
Identity Disposable mailboxes, machine-generated addresses, sub-address farming, VOIP numbers, freight forwarders
Integrity Replayed payloads, bad signatures, fabricated collection times, missing collectors on endpoints that require them

Positive signals matter too: returning devices, verified human interaction, and consistent environments carry negative weight and pull scores down.


Who it deliberately lets through

This is a design commitment, and it is tested:

  • Screen-reader, switch-access, eye-gaze, voice-control, and keyboard-only users. They produce zero pointer movement and paste-only input — an exact match for a naive bot signature. All pointer-absence rules are suppressed when assistive technology is detected. No verdict may rest on pointer absence alone.
  • In-app browser shoppers (Instagram, TikTok, Facebook, LINE, WeChat), which carry a large share of direct-to-consumer traffic and otherwise trip several headless heuristics at once.
  • Consumer VPN users. Using a VPN is weakly correlated with fraud and enormously common. It scores 12 points, once — the timezone, language, and billing-country mismatches it necessarily causes are not counted again, because they are all the same fact.
  • Privacy-hardened browsers. Brave and Firefox randomize canvas and audio by default. That is detected and handled by substituting a stable placeholder, so those users get a consistent identity instead of a new one on every visit.
  • iCloud Private Relay users, travellers, expats, and people on shared or family devices.

Privacy and compliance

  • Per-tenant salt. Two brands running KnowYourClient derive different identifiers for the same device. Identifiers are not a cross-site tracking vector and cannot be correlated between deployments.
  • No PII in identity. Device identity is derived from hardware and rendering characteristics. Emails and phone numbers are normalized for velocity counting, never stored by this library.
  • Never touches card data. Only a BIN (first 6–8 digits) and an opaque PSP token. Behavioural telemetry explicitly excludes payment and password fields — a PCI boundary, not a preference.
  • No raw behavioural biometrics leave the page. Only aggregate statistics — means and variances computed online. There is no keylogging: key identity is never recorded, only rhythm.
  • You choose the lawful basis. Fraud prevention has a recognized legitimate-interest basis in most regimes, but this is your assessment to make. Consent banners are explicitly out of scope — wire the collector behind whatever gate you already use.

Upgrading from v2

v2's KYC.detect() still works via detectAsync(), but the fingerprint semantics changed on purpose. v2 hashed the full User-Agent, the DST-sensitive timezone offset, and the zoom-sensitive pixel ratio into a single value, so its identifier dissolved on a browser update, a season change, or a zoom. v3 derives deviceId only from durable components and matches drifted vectors fuzzily.

v2 identifiers cannot be migrated — they were not stable enough to be worth carrying.

Several v2 bugs are fixed here, including a detectAsync() call that could hang forever when audio rendering was blocked, and a storage probe that destroyed any application data stored under the key __kyc__.


API

Browsercollect(options) · assess(options) · protect(options) · onChange(cb) · BehaviorCollector · detectAccessibility()

ServerEngine · engine.assess({ payload, server, event }) · engine.issueNonce() · MemoryStore · VelocityEngine · resolveIdentity() · classifyIp() · buildServerContext() · expressAssess() · expressNonce() · expressGuard()

Core — reason registry, evaluate(), score(), compareVectors(), deviceId(), normalizeEmail(), ipPrefix(), velocity specs, consistency checks


Scaling

MemoryStore needs no infrastructure and is correct for a single process. It is explicitly not shared across processes — a multi-instance deployment must implement the Store interface against Redis or Postgres, or velocity counters fragment and an attacker spread across N workers sees every threshold multiplied by N.

import type { Store } from '@knowyourclient/server';
class RedisStore implements Store { /* record, count, countDistinct, claim, … */ }

claim() must be genuinely atomic (SET NX) — it is what makes replay detection correct under concurrency.


Honest limitations

  • A valid signature does not prove honest collection. Any key the browser holds, an attacker holds. Signing and nonces raise the cost of forgery and make it detectable; they do not make client data trustworthy. That is why provenance discounting exists.
  • TLS fingerprints are harder to forge from page JavaScript, not impossible to forge. A custom client can emulate a browser's TLS stack. They are valuable because they are out of reach of an in-page spoofing layer.
  • Only JA4 is supported, deliberately. The rest of the JA4+ suite is under a licence that is not permissive for monetization and is patent pending, so this MIT library does not implement it.
  • Shopify checkout cannot run a JS collector. Use the server-side integration there.
  • Thresholds shipped here are starting points. Measure your own traffic in shadow mode first.

Contributing

Issues and pull requests welcome. New detections need a test that proves they fire on the attack and a test that proves they do not fire on a legitimate population.

License

MIT.

Made with ❤️ by Akhlas

About

Stop card testing, bot traffic, and fake signups on any site. Device identity that survives cleared cookies and rotating IPs, velocity across device/subnet/ASN/BIN, and a reason code behind every verdict. Drop-in script tag, self-hosted, MIT.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages