Use case

Tell automation from real users at the TLS layer — before your app parses a request, checks a password, or spends a database query. The fingerprint arrives in the first packet of every connection; this turns it into a decision.

The signal

The corpus is gathered inside a residential proxy network, so a fingerprint seen here is software that routes through anonymising proxies — and ranges across many domains doing it. That population is disproportionately scrapers, checkout bots, credential stuffing. Browsers are the exception: they have an honest reason to be proxied (a VPN) and they run JavaScript, so you can challenge them. Everything else — curl, Go's net/http, headless stacks — can't pass a JS challenge and has no honest reason to be in a proxy pool at volume.

One call: /verdict

Hand it the base64 of a raw ClientHello record; get back the fingerprint and one decision. No detail to sift, unlike the rest of the API — a gate has to decide fast.

curl -s https://api.tls-reputation.com/api/v1/verdict \
  -H 'content-type: application/json' \
  -d '{"client_hello":"<base64 ClientHello>"}'
{
  "ja4": "t13d1516h2_8daaf6152771_b0da82dd1658",
  "verdict": "allow",
  "known": "Google Chrome",
  "browser": true,
  "observed": true,
  "reason": "recognised Google Chrome"
}
verdictwhendo
allowrecognised client, or browser-shaped (carries the post-quantum key share almost only browsers send)let through
challengeunrecognised, or seen but not clearly rangingJS challenge / CAPTCHA
denyunrecognised and reaching many domains through the proxy networkblock or hard-challenge

A reverse-proxy gate

Put a thin reverse proxy in front of your origin. It peeks the ClientHello before the TLS handshake consumes it, asks /verdict once per session, and pins the answer to the connection's JA4 in a signed cookie. Every later request rides the cookie — no API call — and the origin just reads a header.

on each request:
  hello = peek_client_hello(conn)        # MSG_PEEK, before the TLS handshake
  ja4   = ja4(hello)                     # computed at the gate, not asserted

  if cookie.valid and cookie.ja4 == ja4:
      decision = cookie.verdict          # cached — no API call this request
  else:
      decision = POST /verdict {hello}   # once per session
      set_cookie(sign({ ja4, decision }))# JWT, pinned to ja4, stateless

  if decision == "deny":       block
  if decision == "challenge":  js_challenge()   # a real browser passes; headless doesn't
  forward(origin, add_headers = {
      "X-TLS-JA4":     ja4,
      "X-TLS-Verdict": decision,
  })

The cookie is a JWT, so the gate keeps no state; and it is bound to the JA4 the gate recomputes each request, so a cookie lifted onto another client fails the check and gets re-judged — it isn't portable. JA4 (not JA3) because it stays constant across the ClientHello shuffling browsers do, so one browser keeps one JA4 for the session and the cache actually hits.

A working single-file PoC of exactly this lives in the project repo under gate/.

The honest limit

A fingerprint is not a secret. A JA4 is the same for everyone on a given Chrome build, and a determined client can spoof a browser's ClientHello and earn an allow. That's the point of the layering: the fingerprint is the cheap first filter that decides who to challenge, and the JS challenge is what a forged ClientHello still can't get past. The verdict is a signal, never the last word — see the idea and your own fingerprint.