[Examples & benchmarks](https://cdpfleet.com/docs/cases) / #81 · 2026-11-04 · Benchmark · Medium · for [web scraping](https://cdpfleet.com/web-scraping), [ai agents](https://cdpfleet.com/ai-agents)

# Behaviour scoring: Camoufox humanize on and off vs plain Chrome clicks on a public bot-test form

A public behavioural bot-test page scores how a demo login form was filled in: clicks, keystrokes, timing. Camoufox with \`humanize\` off, on and slowed down, and headful Chrome with plain Playwright clicks, each filled it the same way three times, typing fast and typing paced. What the page flagged, what it measured, and whether Google's reCAPTCHA v3 demo moved after real cursor moves and a click. Dated 2026-11-04.

[Camoufox](https://cdpfleet.com/docs/browsers/camoufox) [Google Chrome](https://cdpfleet.com/docs/browsers/chrome)

Run on production on 2026-11-04: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

A benchmark: the results below are what this setup measured on the dates shown, on our own test runs. Sites, networks and browsers change, so rerun the code before you rely on the numbers.

## The problem

Fingerprint checks look at what a browser is; behaviour checks look at what it does — whether the cursor travelled before a click, where the click landed, whether the events were trusted, how fast the keys came. Camoufox's `humanize` option moves the cursor along a curved path instead of jumping, and it is on by default. Does that change what a behaviour-scoring page concludes? [Case 68](https://cdpfleet.com/docs/cases/captcha-scores-by-browser) loaded Google's reCAPTCHA v3 demo with nothing clicked and every setup scored 0.9, so that demo alone can't answer it. This case fills in a public demo form built for exactly this test, with the same actions in every setup, and records the page's verdict, the flags it raised and what it measured. It is a dated measurement on public test pages, not an evasion guide.

## What we used, and why

| What | Why |
| --- | --- |
| Four setups | Camoufox (based on Firefox source; not affiliated with Mozilla) with `humanize: false`, `humanize: true` (the default: a curved cursor path of up to 1.5 s) and `humanize: 3` (up to 3 s), all `headless: true`; and headful Google Chrome as the reference, driven by plain Playwright. Every launch uses `record: false`. |
| `proxy`: `cdpfleet-resi-country-us-session-<id>` | One **sticky** residential exit for the whole run, shared by every setup, so a difference between rows is the browser and how it was driven, not the IP. The form page says it uses no IP reputation; the exit only keeps the network the same. |
| deviceandbrowserinfo.com's behavioural bot test | A public demo login form (`/are_you_a_bot_interactions`) that records clicks, keystrokes and timing; on submit it posts them with a fingerprint to its own endpoint and shows "You are human" or "You are a bot" with the flags behind it. The page says repeated tests don't affect the result. Any email and password are fine; nothing is logged in to. |
| `page.click()` and `keyboard.type()` | The same five actions in every setup: click the email field, type `lab81@example.com`, click the password field, type an 11-character password, click Login. **Fast** typing sends keys with no delay, **paced** with 120 ms between keys. `humanize` changes how the cursor gets to each click target, not where the click lands or how keys are typed. |
| `page.on("request")` | Reads the body the page posts to its own endpoint, so the table can show what the page itself measured: time from the first field interaction to submit, whether each click hit the exact centre of its target, untrusted events, its CDP mouse-coordinate check, the median gap between keys. Nothing is changed or blocked. |
| `page.evaluate()` | Counts `mousemove` events on the form page, and reads the verdict the page prints. |
| Google's reCAPTCHA v3 demo | In each session: load the demo, wait for the score it asks for on load, move the cursor twice and click its "Try again" link, read the score of the reloaded page. |

## How it works

1.  Three rounds; in each, the four setups one after another — one session at a time, never in parallel.
2.  In each session, open the behavioural test form in a fresh context, fill it in with fast typing and submit; read the verdict (up to 30 s). Then the same in another fresh context with paced typing.
3.  In a third fresh context, load the reCAPTCHA v3 demo, move the cursor, click "Try again", and read the new score.
4.  A page load that fails on the network is tried once more; a verdict is never retried.

## The code

The same program in five languages (also [on GitHub](https://github.com/cdpfleet/cdpfleet/tree/main/cases/81-behaviour-scoring-humanize), with the raw output). Set these environment variables first:

-   `CDPFLEET_API_KEY` — your API key (dashboard → API keys)

 

```node
// npm install playwright@1.60.0
// env: CDPFLEET_API_KEY (one session at a time; public test pages only; nothing is solved or retried to get a result)
import { randomBytes } from 'node:crypto';
import { chromium, firefox } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;
// One sticky residential exit for the whole run, shared by every setup: a difference between rows is the browser
// and how it was driven, not the IP.
const PROXY = `cdpfleet-resi-country-us-session-c81${randomBytes(3).toString('hex')}-lifetime-30`;
const REPEATS = 3;
// Camoufox's humanize option: false = the cursor jumps straight to the target, true (the default) = a curved path of
// up to 1.5 s, a number = a path of up to that many seconds. Chrome is the reference: plain Playwright clicks.
const SETUPS = [
  { key: 'cf_off', setup: 'Camoufox, humanize: false', engine: 'camoufox', type: firefox, body: { headless: true, humanize: false } },
  { key: 'cf_on', setup: 'Camoufox, humanize: true (default)', engine: 'camoufox', type: firefox, body: { headless: true, humanize: true } },
  { key: 'cf_slow', setup: 'Camoufox, humanize: 3', engine: 'camoufox', type: firefox, body: { headless: true, humanize: 3 } },
  { key: 'chrome', setup: 'Google Chrome, headful', engine: 'chrome', type: chromium, body: { headless: false } },
];
// The same actions in every cell; only the typing speed differs between the two form runs of a session.
const TYPING = { fast: 0, paced: 120 }; // ms between keys
const EMAIL = 'lab81@example.com';
const PASSWORD = 'lab-81!pass'; // the demo form wants 8+ characters with a special character

// deviceandbrowserinfo.com's behavioural bot test: a demo login form that records clicks, keystrokes and timing
// and, on submit, posts them with a fingerprint to its own endpoint, which answers human or bot with its flags.
const FORM = 'https://deviceandbrowserinfo.com/are_you_a_bot_interactions';
// Google's public reCAPTCHA v3 demo prints its server's verify response (with the score); its "Try again" link reloads it.
const RECAPTCHA = 'https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php';

// The same expressions run in every language.
// Count mousemove events on the page, so the table shows what humanize changed.
const COUNT_MOVES = `(() => { window.__lab81 = 0; document.addEventListener('mousemove', () => { window.__lab81++; }, true); return true; })()`;
// The verdict the page shows after submit: isBot and every detail flag that is true.
const VERDICT = `(() => {
  const e = document.getElementById('jsonResult');
  const t = e ? e.innerText : '';
  if (t.length < 10) return null;
  const r = JSON.parse(t);
  const d = r.details || {};
  return { is_bot: r.isBot === true, flags: Object.keys(d).filter((k) => d[k] === true), moves: window.__lab81 };
})()`;
// What the page itself measured about the interaction: the "interactions" part of the body it posted, which is
// embedded as a JSON string and passed to this function.
const MEASURED = `(body) => {
  const i = JSON.parse(body).interactions || {};
  const s = i.emailTimingStats || {};
  return {
    time_to_fill_ms: Math.round(i.timeToFill),
    exact_center_clicks: [i.hasClickedEmailFieldExactCenter, i.hasClickedPasswordFieldExactCenter, i.hasClickedSubmitExactCenter].filter(Boolean).length,
    untrusted_event: i.hasUntrustedEvent === true,
    cdp_mouse_leak: i.cdpMouseLeak === true,
    key_gap_median_ms: typeof s.medianTimeBetweenKeys === 'number' ? Math.round(s.medianTimeBetweenKeys) : null,
  };
}`;
const SCORE = `(() => { if (window.__lab81rc) return null; const m = /"score"\\s*:\\s*([0-9.]+)/.exec(document.body ? document.body.innerText : ''); return m ? Number(m[1]) : null; })()`;

async function launch(s) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(`https://starter.cdpfleet.com/${s.engine}/session`, {
      method: 'POST', headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
      body: JSON.stringify({ proxy: PROXY, record: false, ...s.body }),
    });
    if (res.ok) return s.type.connect((await res.json()).wsUrl, { headers: { 'x-api-key': KEY } });
    if (![429, 503].includes(res.status) || attempt === 5) throw new Error(`launch ${res.status} ${await res.text()}`);
    await new Promise((r) => setTimeout(r, (Number(res.headers.get('retry-after')) || 5) * 1000));
  }
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

// A load that fails on the network (proxy timeout) is tried once more; the page's answer is never retried.
async function open(tab, url) {
  for (let attempt = 1; attempt <= 2; attempt++) {
    try {
      await tab.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
      return true;
    } catch { /* network failure */ }
  }
  return false;
}

// One form run: click the email field, type, click the password field, type, click Login; read the verdict.
async function form(browser, typing) {
  const out = { typing, verdict: 'error', flags: [], moves: null, action_seconds: null, measured: null };
  const context = await browser.newContext(); // a fresh context: every run is a first visit
  try {
    const tab = await context.newPage();
    let posted = null;
    tab.on('request', (req) => { if (req.url().includes('/fingerprint_bot_test')) posted = req.postData(); });
    if (!(await open(tab, FORM))) return out;
    await sleep(1000);
    await tab.evaluate(COUNT_MOVES);
    const t0 = Date.now();
    await tab.click('#email');
    await tab.keyboard.type(EMAIL, { delay: TYPING[typing] });
    await tab.click('#password');
    await tab.keyboard.type(PASSWORD, { delay: TYPING[typing] });
    await tab.click('#loginForm button[type=submit]');
    out.action_seconds = Math.round((Date.now() - t0) / 100) / 10;
    let v = null;
    for (let i = 0; i < 60 && v === null; i++) { await sleep(500); v = await tab.evaluate(VERDICT).catch(() => null); }
    if (v === null) { out.verdict = 'no verdict'; return out; }
    Object.assign(out, { verdict: v.is_bot ? 'bot' : 'human', flags: v.flags, moves: v.moves });
    if (posted) out.measured = await tab.evaluate(`(${MEASURED})(${JSON.stringify(posted)})`);
    return out;
  } catch {
    return out;
  } finally {
    await context.close().catch(() => {});
  }
}

// Google's v3 demo: wait for the score it asks for on load, move the cursor and click "Try again", read the new score.
async function recaptcha(browser) {
  const out = { before: null, after: null };
  const context = await browser.newContext();
  try {
    const tab = await context.newPage();
    if (!(await open(tab, RECAPTCHA))) return out;
    for (let i = 0; i < 40 && out.before === null; i++) { await sleep(500); out.before = await tab.evaluate(SCORE).catch(() => null); }
    await tab.evaluate('window.__lab81rc = 1'); // marks this document, so the score is read from the reloaded one
    await tab.mouse.move(300, 300);
    await tab.mouse.move(600, 200);
    await tab.click('a[href="/recaptcha-v3-request-scores.php"]');
    for (let i = 0; i < 40 && out.after === null; i++) { await sleep(500); out.after = await tab.evaluate(SCORE).catch(() => null); }
    return out;
  } catch {
    return out;
  } finally {
    await context.close().catch(() => {});
  }
}

const runs = [];
const scores = [];
// Serial: one session at a time; the repeats are interleaved so a slow minute doesn't land on one setup.
for (let repeat = 1; repeat <= REPEATS; repeat++) {
  for (const s of SETUPS) {
    const base = { setup: s.setup, key: s.key, repeat };
    let browser;
    try {
      browser = await launch(s);
    } catch {
      for (const typing of Object.keys(TYPING)) runs.push({ ...base, typing, verdict: 'error', flags: [], moves: null, action_seconds: null, measured: null });
      scores.push({ ...base, before: null, after: null });
      continue;
    }
    for (const typing of Object.keys(TYPING)) runs.push({ ...base, ...(await form(browser, typing)) });
    scores.push({ ...base, ...(await recaptcha(browser)) });
    await browser.close().catch(() => {});
  }
}
console.log(JSON.stringify({ measured_at: new Date().toISOString().slice(0, 10), runs, recaptcha: scores }, null, 2));
```

```python
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY (one session at a time; public test pages only; nothing is solved or retried to get a result)
import json
import os
import secrets
import time
from datetime import datetime, timezone

import requests
from playwright.sync_api import sync_playwright

KEY = os.environ["CDPFLEET_API_KEY"]
# One sticky residential exit for the whole run, shared by every setup: a difference between rows is the browser
# and how it was driven, not the IP.
PROXY = f"cdpfleet-resi-country-us-session-c81{secrets.token_hex(3)}-lifetime-30"
REPEATS = 3
# Camoufox's humanize option: False = the cursor jumps straight to the target, True (the default) = a curved path of
# up to 1.5 s, a number = a path of up to that many seconds. Chrome is the reference: plain Playwright clicks.
SETUPS = [
    {"key": "cf_off", "setup": "Camoufox, humanize: false", "engine": "camoufox", "type": "firefox", "body": {"headless": True, "humanize": False}},
    {"key": "cf_on", "setup": "Camoufox, humanize: true (default)", "engine": "camoufox", "type": "firefox", "body": {"headless": True, "humanize": True}},
    {"key": "cf_slow", "setup": "Camoufox, humanize: 3", "engine": "camoufox", "type": "firefox", "body": {"headless": True, "humanize": 3}},
    {"key": "chrome", "setup": "Google Chrome, headful", "engine": "chrome", "type": "chromium", "body": {"headless": False}},
]
# The same actions in every cell; only the typing speed differs between the two form runs of a session.
TYPING = {"fast": 0, "paced": 120}  # ms between keys
EMAIL = "lab81@example.com"
PASSWORD = "lab-81!pass"  # the demo form wants 8+ characters with a special character

# deviceandbrowserinfo.com's behavioural bot test: a demo login form that records clicks, keystrokes and timing
# and, on submit, posts them with a fingerprint to its own endpoint, which answers human or bot with its flags.
FORM = "https://deviceandbrowserinfo.com/are_you_a_bot_interactions"
# Google's public reCAPTCHA v3 demo prints its server's verify response (with the score); its "Try again" link reloads it.
RECAPTCHA = "https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php"

# The same expressions run in every language.
# Count mousemove events on the page, so the table shows what humanize changed.
COUNT_MOVES = "(() => { window.__lab81 = 0; document.addEventListener('mousemove', () => { window.__lab81++; }, true); return true; })()"
# The verdict the page shows after submit: isBot and every detail flag that is true.
VERDICT = """(() => {
  const e = document.getElementById('jsonResult');
  const t = e ? e.innerText : '';
  if (t.length < 10) return null;
  const r = JSON.parse(t);
  const d = r.details || {};
  return { is_bot: r.isBot === true, flags: Object.keys(d).filter((k) => d[k] === true), moves: window.__lab81 };
})()"""
# What the page itself measured about the interaction: the "interactions" part of the body it posted, which is
# embedded as a JSON string and passed to this function.
MEASURED = """(body) => {
  const i = JSON.parse(body).interactions || {};
  const s = i.emailTimingStats || {};
  return {
    time_to_fill_ms: Math.round(i.timeToFill),
    exact_center_clicks: [i.hasClickedEmailFieldExactCenter, i.hasClickedPasswordFieldExactCenter, i.hasClickedSubmitExactCenter].filter(Boolean).length,
    untrusted_event: i.hasUntrustedEvent === true,
    cdp_mouse_leak: i.cdpMouseLeak === true,
    key_gap_median_ms: typeof s.medianTimeBetweenKeys === 'number' ? Math.round(s.medianTimeBetweenKeys) : null,
  };
}"""
SCORE = r"""(() => { if (window.__lab81rc) return null; const m = /"score"\s*:\s*([0-9.]+)/.exec(document.body ? document.body.innerText : ''); return m ? Number(m[1]) : null; })()"""

def launch(p, s):
    body = {"proxy": PROXY, "record": False, **s["body"]}
    for attempt in range(1, 6):
        res = requests.post(f"https://starter.cdpfleet.com/{s['engine']}/session", headers={"x-api-key": KEY}, json=body, timeout=120)
        if res.ok:
            return getattr(p, s["type"]).connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
        if res.status_code not in (429, 503) or attempt == 5:
            raise RuntimeError(f"launch {res.status_code} {res.text}")
        time.sleep(int(res.headers.get("retry-after") or 5))

def safe(tab, expr):
    try:
        return tab.evaluate(expr)
    except Exception:
        return None

# A load that fails on the network (proxy timeout) is tried once more; the page's answer is never retried.
def open_page(tab, url):
    for _ in range(2):
        try:
            tab.goto(url, wait_until="domcontentloaded", timeout=30000)
            return True
        except Exception:
            pass
    return False

# One form run: click the email field, type, click the password field, type, click Login; read the verdict.
def form(browser, typing):
    out = {"typing": typing, "verdict": "error", "flags": [], "moves": None, "action_seconds": None, "measured": None}
    context = browser.new_context()  # a fresh context: every run is a first visit
    try:
        tab = context.new_page()
        posted = {}

        def on_request(req):
            if "/fingerprint_bot_test" in req.url:
                posted["body"] = req.post_data

        tab.on("request", on_request)
        if not open_page(tab, FORM):
            return out
        time.sleep(1)
        tab.evaluate(COUNT_MOVES)
        t0 = time.time()
        tab.click("#email")
        tab.keyboard.type(EMAIL, delay=TYPING[typing])
        tab.click("#password")
        tab.keyboard.type(PASSWORD, delay=TYPING[typing])
        tab.click("#loginForm button[type=submit]")
        out["action_seconds"] = round((time.time() - t0) * 10) / 10
        v = None
        for _ in range(60):
            time.sleep(0.5)
            v = safe(tab, VERDICT)
            if v is not None:
                break
        if v is None:
            out["verdict"] = "no verdict"
            return out
        out.update({"verdict": "bot" if v["is_bot"] else "human", "flags": v["flags"], "moves": v["moves"]})
        if posted.get("body"):
            out["measured"] = tab.evaluate(f"({MEASURED})({json.dumps(posted['body'])})")
        return out
    except Exception:
        return out
    finally:
        try:
            context.close()
        except Exception:
            pass

# Google's v3 demo: wait for the score it asks for on load, move the cursor and click "Try again", read the new score.
def recaptcha(browser):
    out = {"before": None, "after": None}
    context = browser.new_context()
    try:
        tab = context.new_page()
        if not open_page(tab, RECAPTCHA):
            return out
        for _ in range(40):
            time.sleep(0.5)
            out["before"] = safe(tab, SCORE)
            if out["before"] is not None:
                break
        tab.evaluate("window.__lab81rc = 1")  # marks this document, so the score is read from the reloaded one
        tab.mouse.move(300, 300)
        tab.mouse.move(600, 200)
        tab.click('a[href="/recaptcha-v3-request-scores.php"]')
        for _ in range(40):
            time.sleep(0.5)
            out["after"] = safe(tab, SCORE)
            if out["after"] is not None:
                break
        return out
    except Exception:
        return out
    finally:
        try:
            context.close()
        except Exception:
            pass

runs = []
scores = []
with sync_playwright() as p:
    # Serial: one session at a time; the repeats are interleaved so a slow minute doesn't land on one setup.
    for repeat in range(1, REPEATS + 1):
        for s in SETUPS:
            base = {"setup": s["setup"], "key": s["key"], "repeat": repeat}
            try:
                browser = launch(p, s)
            except Exception:
                for typing in TYPING:
                    runs.append({**base, "typing": typing, "verdict": "error", "flags": [], "moves": None, "action_seconds": None, "measured": None})
                scores.append({**base, "before": None, "after": None})
                continue
            for typing in TYPING:
                runs.append({**base, **form(browser, typing)})
            scores.append({**base, **recaptcha(browser)})
            try:
                browser.close()
            except Exception:
                pass

print(json.dumps({"measured_at": datetime.now(timezone.utc).strftime("%Y-%m-%d"), "runs": runs, "recaptcha": scores}, indent=2))
```

```java
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1.
// env: CDPFLEET_API_KEY (one session at a time; public test pages only; nothing is solved or retried to get a result)
import com.google.gson.*;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.WaitUntilState;
import java.net.URI;
import java.net.http.*;
import java.security.SecureRandom;
import java.time.Instant;
import java.util.*;

public class Main {
  static final String KEY = System.getenv("CDPFLEET_API_KEY");
  static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().serializeNulls().create();
  static final HttpClient HTTP = HttpClient.newHttpClient();
  // One sticky residential exit for the whole run, shared by every setup: a difference between rows is the browser
  // and how it was driven, not the IP.
  static final String PROXY = String.format("cdpfleet-resi-country-us-session-c81%06x-lifetime-30", new SecureRandom().nextInt(1 << 24));
  static final int REPEATS = 3;

  record Setup(String key, String name, String engine, String type, boolean headless, Object humanize) {}

  // Camoufox's humanize option: false = the cursor jumps straight to the target, true (the default) = a curved path of
  // up to 1.5 s, a number = a path of up to that many seconds. Chrome is the reference: plain Playwright clicks.
  static final List<Setup> SETUPS = List.of(
      new Setup("cf_off", "Camoufox, humanize: false", "camoufox", "firefox", true, false),
      new Setup("cf_on", "Camoufox, humanize: true (default)", "camoufox", "firefox", true, true),
      new Setup("cf_slow", "Camoufox, humanize: 3", "camoufox", "firefox", true, 3),
      new Setup("chrome", "Google Chrome, headful", "chrome", "chromium", false, null));
  // The same actions in every cell; only the typing speed differs between the two form runs of a session.
  static final LinkedHashMap<String, Double> TYPING = new LinkedHashMap<>(); // ms between keys
  static {
    TYPING.put("fast", 0.0);
    TYPING.put("paced", 120.0);
  }
  static final String EMAIL = "lab81@example.com";
  static final String PASSWORD = "lab-81!pass"; // the demo form wants 8+ characters with a special character

  // deviceandbrowserinfo.com's behavioural bot test: a demo login form that records clicks, keystrokes and timing
  // and, on submit, posts them with a fingerprint to its own endpoint, which answers human or bot with its flags.
  static final String FORM = "https://deviceandbrowserinfo.com/are_you_a_bot_interactions";
  // Google's public reCAPTCHA v3 demo prints its server's verify response (with the score); its "Try again" link reloads it.
  static final String RECAPTCHA = "https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php";

  // The same expressions run in every language.
  // Count mousemove events on the page, so the table shows what humanize changed.
  static final String COUNT_MOVES = "(() => { window.__lab81 = 0; document.addEventListener('mousemove', () => { window.__lab81++; }, true); return true; })()";
  // The verdict the page shows after submit: isBot and every detail flag that is true.
  static final String VERDICT = """
      (() => {
        const e = document.getElementById('jsonResult');
        const t = e ? e.innerText : '';
        if (t.length < 10) return null;
        const r = JSON.parse(t);
        const d = r.details || {};
        return { is_bot: r.isBot === true, flags: Object.keys(d).filter((k) => d[k] === true), moves: window.__lab81 };
      })()""";
  // What the page itself measured about the interaction: the "interactions" part of the body it posted, which is
  // embedded as a JSON string and passed to this function.
  static final String MEASURED = """
      (body) => {
        const i = JSON.parse(body).interactions || {};
        const s = i.emailTimingStats || {};
        return {
          time_to_fill_ms: Math.round(i.timeToFill),
          exact_center_clicks: [i.hasClickedEmailFieldExactCenter, i.hasClickedPasswordFieldExactCenter, i.hasClickedSubmitExactCenter].filter(Boolean).length,
          untrusted_event: i.hasUntrustedEvent === true,
          cdp_mouse_leak: i.cdpMouseLeak === true,
          key_gap_median_ms: typeof s.medianTimeBetweenKeys === 'number' ? Math.round(s.medianTimeBetweenKeys) : null,
        };
      }""";
  static final String SCORE = "(() => { if (window.__lab81rc) return null; const m = /\"score\"\\s*:\\s*([0-9.]+)/.exec(document.body ? document.body.innerText : ''); return m ? Number(m[1]) : null; })()";

  static Playwright playwright;

  static Browser launch(Setup s) throws Exception {
    JsonObject body = new JsonObject();
    body.addProperty("proxy", PROXY);
    body.addProperty("record", false);
    body.addProperty("headless", s.headless());
    if (s.humanize() instanceof Boolean b) body.addProperty("humanize", b);
    else if (s.humanize() instanceof Number n) body.addProperty("humanize", n);
    for (int attempt = 1; ; attempt++) {
      HttpResponse<String> res = HTTP.send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/" + s.engine() + "/session"))
          .header("x-api-key", KEY).header("content-type", "application/json")
          .POST(HttpRequest.BodyPublishers.ofString(body.toString())).build(), HttpResponse.BodyHandlers.ofString());
      if (res.statusCode() == 200) {
        String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
        BrowserType type = s.type().equals("firefox") ? playwright.firefox() : playwright.chromium();
        return type.connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
      }
      if ((res.statusCode() != 429 && res.statusCode() != 503) || attempt == 5) throw new RuntimeException("launch " + res.statusCode() + " " + res.body());
      Thread.sleep(Long.parseLong(res.headers().firstValue("retry-after").orElse("5")) * 1000);
    }
  }

  static Object safe(Page tab, String expr) {
    try { return tab.evaluate(expr); } catch (PlaywrightException err) { return null; }
  }

  // A load that fails on the network (proxy timeout) is tried once more; the page's answer is never retried.
  static boolean open(Page tab, String url) {
    for (int attempt = 1; attempt <= 2; attempt++) {
      try {
        tab.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED).setTimeout(30000));
        return true;
      } catch (PlaywrightException err) { /* network failure */ }
    }
    return false;
  }

  static JsonObject emptyRun(JsonObject base, String typing) {
    JsonObject out = base.deepCopy();
    out.addProperty("typing", typing);
    out.addProperty("verdict", "error");
    out.add("flags", new JsonArray());
    out.add("moves", JsonNull.INSTANCE);
    out.add("action_seconds", JsonNull.INSTANCE);
    out.add("measured", JsonNull.INSTANCE);
    return out;
  }

  // One form run: click the email field, type, click the password field, type, click Login; read the verdict.
  static JsonObject form(Browser browser, JsonObject base, String typing) throws Exception {
    JsonObject out = emptyRun(base, typing);
    BrowserContext context = browser.newContext(); // a fresh context: every run is a first visit
    try {
      Page tab = context.newPage();
      String[] posted = {null};
      tab.onRequest(req -> { if (req.url().contains("/fingerprint_bot_test")) posted[0] = req.postData(); });
      if (!open(tab, FORM)) return out;
      Thread.sleep(1000);
      tab.evaluate(COUNT_MOVES);
      long t0 = System.currentTimeMillis();
      double delay = TYPING.get(typing);
      tab.click("#email");
      tab.keyboard().type(EMAIL, new Keyboard.TypeOptions().setDelay(delay));
      tab.click("#password");
      tab.keyboard().type(PASSWORD, new Keyboard.TypeOptions().setDelay(delay));
      tab.click("#loginForm button[type=submit]");
      out.addProperty("action_seconds", Math.round((System.currentTimeMillis() - t0) / 100.0) / 10.0);
      Object v = null;
      for (int i = 0; i < 60 && v == null; i++) { Thread.sleep(500); v = safe(tab, VERDICT); }
      if (v == null) { out.addProperty("verdict", "no verdict"); return out; }
      JsonObject verdict = GSON.toJsonTree(v).getAsJsonObject();
      out.addProperty("verdict", verdict.get("is_bot").getAsBoolean() ? "bot" : "human");
      out.add("flags", verdict.get("flags"));
      out.addProperty("moves", verdict.get("moves").getAsInt());
      if (posted[0] != null) {
        JsonObject m = GSON.toJsonTree(tab.evaluate("(" + MEASURED + ")(" + new Gson().toJson(posted[0]) + ")")).getAsJsonObject();
        JsonObject measured = new JsonObject();
        measured.addProperty("time_to_fill_ms", m.get("time_to_fill_ms").getAsInt());
        measured.addProperty("exact_center_clicks", m.get("exact_center_clicks").getAsInt());
        measured.add("untrusted_event", m.get("untrusted_event"));
        measured.add("cdp_mouse_leak", m.get("cdp_mouse_leak"));
        JsonElement gap = m.get("key_gap_median_ms");
        if (gap == null || gap.isJsonNull()) measured.add("key_gap_median_ms", JsonNull.INSTANCE);
        else measured.addProperty("key_gap_median_ms", gap.getAsInt());
        out.add("measured", measured);
      }
      return out;
    } catch (RuntimeException err) { // a PlaywrightException, or a verdict without a field
      return out;
    } finally {
      try { context.close(); } catch (PlaywrightException err) { }
    }
  }

  static void putScore(JsonObject out, String name, Object score) {
    if (score == null) out.add(name, JsonNull.INSTANCE); else out.addProperty(name, ((Number) score).doubleValue());
  }

  // Google's v3 demo: wait for the score it asks for on load, move the cursor and click "Try again", read the new score.
  static JsonObject recaptcha(Browser browser, JsonObject base) throws Exception {
    JsonObject out = base.deepCopy();
    Object before = null, after = null;
    BrowserContext context = browser.newContext();
    try {
      Page tab = context.newPage();
      if (open(tab, RECAPTCHA)) {
        for (int i = 0; i < 40 && before == null; i++) { Thread.sleep(500); before = safe(tab, SCORE); }
        tab.evaluate("window.__lab81rc = 1"); // marks this document, so the score is read from the reloaded one
        tab.mouse().move(300, 300);
        tab.mouse().move(600, 200);
        tab.click("a[href=\"/recaptcha-v3-request-scores.php\"]");
        for (int i = 0; i < 40 && after == null; i++) { Thread.sleep(500); after = safe(tab, SCORE); }
      }
    } catch (PlaywrightException err) {
      // keep what was read
    } finally {
      try { context.close(); } catch (PlaywrightException err) { }
    }
    putScore(out, "before", before);
    putScore(out, "after", after);
    return out;
  }

  public static void main(String[] args) throws Exception {
    playwright = Playwright.create();
    JsonArray runs = new JsonArray();
    JsonArray scores = new JsonArray();
    // Serial: one session at a time; the repeats are interleaved so a slow minute doesn't land on one setup.
    for (int repeat = 1; repeat <= REPEATS; repeat++) {
      for (Setup s : SETUPS) {
        JsonObject base = new JsonObject();
        base.addProperty("setup", s.name());
        base.addProperty("key", s.key());
        base.addProperty("repeat", repeat);
        Browser browser;
        try {
          browser = launch(s);
        } catch (Exception err) {
          for (String typing : TYPING.keySet()) runs.add(emptyRun(base, typing));
          JsonObject sc = base.deepCopy();
          sc.add("before", JsonNull.INSTANCE);
          sc.add("after", JsonNull.INSTANCE);
          scores.add(sc);
          continue;
        }
        // A session the server ends leaves the remaining cells as errors.
        for (String typing : TYPING.keySet()) {
          try { runs.add(form(browser, base, typing)); } catch (PlaywrightException err) { runs.add(emptyRun(base, typing)); }
        }
        try {
          scores.add(recaptcha(browser, base));
        } catch (PlaywrightException err) {
          JsonObject sc = base.deepCopy();
          sc.add("before", JsonNull.INSTANCE);
          sc.add("after", JsonNull.INSTANCE);
          scores.add(sc);
        }
        try { browser.close(); } catch (PlaywrightException err) { }
      }
    }
    playwright.close();
    JsonObject result = new JsonObject();
    result.addProperty("measured_at", Instant.now().toString().substring(0, 10));
    result.add("runs", runs);
    result.add("recaptcha", scores);
    System.out.println(GSON.toJson(result));
  }
}
```

```csharp
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY (one session at a time; public test pages only; nothing is solved or retried to get a result)
using System.Diagnostics;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;

var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
// One sticky residential exit for the whole run, shared by every setup: a difference between rows is the browser
// and how it was driven, not the IP.
var proxy = $"cdpfleet-resi-country-us-session-c81{Convert.ToHexString(RandomNumberGenerator.GetBytes(3)).ToLowerInvariant()}-lifetime-30";
const int Repeats = 3;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
using var playwright = await Playwright.CreateAsync();

// Camoufox's humanize option: false = the cursor jumps straight to the target, true (the default) = a curved path of
// up to 1.5 s, a number = a path of up to that many seconds. Chrome is the reference: plain Playwright clicks.
var setups = new List<(string Key, string Name, string Engine, IBrowserType Type, JsonObject Body)>
{
    ("cf_off", "Camoufox, humanize: false", "camoufox", playwright.Firefox, new JsonObject { ["headless"] = true, ["humanize"] = false }),
    ("cf_on", "Camoufox, humanize: true (default)", "camoufox", playwright.Firefox, new JsonObject { ["headless"] = true, ["humanize"] = true }),
    ("cf_slow", "Camoufox, humanize: 3", "camoufox", playwright.Firefox, new JsonObject { ["headless"] = true, ["humanize"] = 3 }),
    ("chrome", "Google Chrome, headful", "chrome", playwright.Chromium, new JsonObject { ["headless"] = false }),
};
// The same actions in every cell; only the typing speed differs between the two form runs of a session.
var typing = new List<(string Name, float Delay)> { ("fast", 0), ("paced", 120) }; // ms between keys
const string Email = "lab81@example.com";
const string Password = "lab-81!pass"; // the demo form wants 8+ characters with a special character

// deviceandbrowserinfo.com's behavioural bot test: a demo login form that records clicks, keystrokes and timing
// and, on submit, posts them with a fingerprint to its own endpoint, which answers human or bot with its flags.
const string Form = "https://deviceandbrowserinfo.com/are_you_a_bot_interactions";
// Google's public reCAPTCHA v3 demo prints its server's verify response (with the score); its "Try again" link reloads it.
const string Recaptcha = "https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php";

// The same expressions run in every language.
// Count mousemove events on the page, so the table shows what humanize changed.
const string CountMoves = "(() => { window.__lab81 = 0; document.addEventListener('mousemove', () => { window.__lab81++; }, true); return true; })()";
// The verdict the page shows after submit: isBot and every detail flag that is true.
const string Verdict = """
    (() => {
      const e = document.getElementById('jsonResult');
      const t = e ? e.innerText : '';
      if (t.length < 10) return null;
      const r = JSON.parse(t);
      const d = r.details || {};
      return { is_bot: r.isBot === true, flags: Object.keys(d).filter((k) => d[k] === true), moves: window.__lab81 };
    })()
    """;
// What the page itself measured about the interaction: the "interactions" part of the body it posted, which is
// embedded as a JSON string and passed to this function.
const string Measured = """
    (body) => {
      const i = JSON.parse(body).interactions || {};
      const s = i.emailTimingStats || {};
      return {
        time_to_fill_ms: Math.round(i.timeToFill),
        exact_center_clicks: [i.hasClickedEmailFieldExactCenter, i.hasClickedPasswordFieldExactCenter, i.hasClickedSubmitExactCenter].filter(Boolean).length,
        untrusted_event: i.hasUntrustedEvent === true,
        cdp_mouse_leak: i.cdpMouseLeak === true,
        key_gap_median_ms: typeof s.medianTimeBetweenKeys === 'number' ? Math.round(s.medianTimeBetweenKeys) : null,
      };
    }
    """;
const string Score = """(() => { if (window.__lab81rc) return null; const m = /"score"\s*:\s*([0-9.]+)/.exec(document.body ? document.body.innerText : ''); return m ? Number(m[1]) : null; })()""";

async Task<IBrowser> Launch((string Key, string Name, string Engine, IBrowserType Type, JsonObject Body) s)
{
    var body = new JsonObject { ["proxy"] = proxy, ["record"] = false };
    foreach (var (k, v) in s.Body) body[k] = v?.DeepClone();
    for (var attempt = 1; ; attempt++)
    {
        var res = await http.PostAsJsonAsync($"https://starter.cdpfleet.com/{s.Engine}/session", body);
        if (res.IsSuccessStatusCode)
        {
            var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
            return await s.Type.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
        }
        var code = (int)res.StatusCode;
        if ((code != 429 && code != 503) || attempt == 5) throw new Exception($"launch {code} {await res.Content.ReadAsStringAsync()}");
        await Task.Delay(TimeSpan.FromSeconds(res.Headers.RetryAfter?.Delta?.TotalSeconds ?? 5));
    }
}

// A load that fails on the network (proxy timeout) is tried once more; the page's answer is never retried.
async Task<bool> Open(IPage tab, string url)
{
    for (var attempt = 1; attempt <= 2; attempt++)
    {
        try { await tab.GotoAsync(url, new() { WaitUntil = WaitUntilState.DOMContentLoaded, Timeout = 30000 }); return true; }
        catch (PlaywrightException) { /* network failure */ }
        catch (TimeoutException) { /* network failure */ }
    }
    return false;
}

async Task<JsonElement?> Eval(IPage tab, string expr)
{
    try
    {
        var v = await tab.EvaluateAsync<JsonElement>(expr);
        return v.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined ? null : v;
    }
    catch (PlaywrightException) { return null; }
}

JsonObject EmptyRun(JsonObject @base, string name) => new()
{
    ["setup"] = @base["setup"]!.DeepClone(), ["key"] = @base["key"]!.DeepClone(), ["repeat"] = @base["repeat"]!.DeepClone(),
    ["typing"] = name, ["verdict"] = "error", ["flags"] = new JsonArray(), ["moves"] = null, ["action_seconds"] = null, ["measured"] = null,
};

// One form run: click the email field, type, click the password field, type, click Login; read the verdict.
async Task<JsonObject> FormRun(IBrowser browser, JsonObject @base, string name, float delay)
{
    var output = EmptyRun(@base, name);
    IBrowserContext? context = null;
    try
    {
        context = await browser.NewContextAsync(); // a fresh context: every run is a first visit
        var tab = await context.NewPageAsync();
        string? posted = null;
        tab.Request += (_, req) => { if (req.Url.Contains("/fingerprint_bot_test")) posted = req.PostData; };
        if (!await Open(tab, Form)) return output;
        await Task.Delay(1000);
        await tab.EvaluateAsync(CountMoves);
        var watch = Stopwatch.StartNew();
        await tab.ClickAsync("#email");
        await tab.Keyboard.TypeAsync(Email, new() { Delay = delay });
        await tab.ClickAsync("#password");
        await tab.Keyboard.TypeAsync(Password, new() { Delay = delay });
        await tab.ClickAsync("#loginForm button[type=submit]");
        output["action_seconds"] = Math.Round(watch.Elapsed.TotalSeconds * 10) / 10;
        JsonElement? v = null;
        for (var i = 0; i < 60 && v == null; i++) { await Task.Delay(500); v = await Eval(tab, Verdict); }
        if (v is not { } verdict) { output["verdict"] = "no verdict"; return output; }
        output["verdict"] = verdict.GetProperty("is_bot").GetBoolean() ? "bot" : "human";
        output["flags"] = JsonNode.Parse(verdict.GetProperty("flags").GetRawText());
        output["moves"] = verdict.GetProperty("moves").GetInt32();
        if (posted != null && await Eval(tab, $"({Measured})({JsonSerializer.Serialize(posted)})") is { } m)
        {
            var gap = m.GetProperty("key_gap_median_ms");
            output["measured"] = new JsonObject
            {
                ["time_to_fill_ms"] = m.GetProperty("time_to_fill_ms").GetInt32(),
                ["exact_center_clicks"] = m.GetProperty("exact_center_clicks").GetInt32(),
                ["untrusted_event"] = m.GetProperty("untrusted_event").GetBoolean(),
                ["cdp_mouse_leak"] = m.GetProperty("cdp_mouse_leak").GetBoolean(),
                ["key_gap_median_ms"] = gap.ValueKind == JsonValueKind.Number ? gap.GetInt32() : null,
            };
        }
        return output;
    }
    catch (Exception e) when (e is PlaywrightException or TimeoutException or KeyNotFoundException or InvalidOperationException)
    {
        return output;
    }
    finally
    {
        if (context != null) try { await context.CloseAsync(); } catch (PlaywrightException) { }
    }
}

async Task<double?> ReadScore(IPage tab) => await Eval(tab, Score) is { } v ? v.GetDouble() : null;

// Google's v3 demo: wait for the score it asks for on load, move the cursor and click "Try again", read the new score.
async Task<JsonObject> RecaptchaRun(IBrowser browser, JsonObject @base)
{
    double? before = null, after = null;
    IBrowserContext? context = null;
    try
    {
        context = await browser.NewContextAsync();
        var tab = await context.NewPageAsync();
        if (await Open(tab, Recaptcha))
        {
            for (var i = 0; i < 40 && before == null; i++) { await Task.Delay(500); before = await ReadScore(tab); }
            await tab.EvaluateAsync("window.__lab81rc = 1"); // marks this document, so the score is read from the reloaded one
            await tab.Mouse.MoveAsync(300, 300);
            await tab.Mouse.MoveAsync(600, 200);
            await tab.ClickAsync("a[href=\"/recaptcha-v3-request-scores.php\"]");
            for (var i = 0; i < 40 && after == null; i++) { await Task.Delay(500); after = await ReadScore(tab); }
        }
    }
    catch (Exception e) when (e is PlaywrightException or TimeoutException) { /* keep what was read */ }
    finally
    {
        if (context != null) try { await context.CloseAsync(); } catch (PlaywrightException) { }
    }
    var output = (JsonObject)@base.DeepClone();
    output["before"] = before;
    output["after"] = after;
    return output;
}

var runs = new JsonArray();
var scores = new JsonArray();
// Serial: one session at a time; the repeats are interleaved so a slow minute doesn't land on one setup.
for (var repeat = 1; repeat <= Repeats; repeat++)
{
    foreach (var s in setups)
    {
        var @base = new JsonObject { ["setup"] = s.Name, ["key"] = s.Key, ["repeat"] = repeat };
        IBrowser browser;
        try { browser = await Launch(s); }
        catch (Exception)
        {
            foreach (var t in typing) runs.Add(EmptyRun(@base, t.Name));
            var sc = (JsonObject)@base.DeepClone();
            sc["before"] = null;
            sc["after"] = null;
            scores.Add(sc);
            continue;
        }
        foreach (var t in typing) runs.Add(await FormRun(browser, @base, t.Name, t.Delay));
        scores.Add(await RecaptchaRun(browser, @base));
        try { await browser.CloseAsync(); } catch (PlaywrightException) { }
    }
}

var result = new JsonObject { ["measured_at"] = DateTime.UtcNow.ToString("yyyy-MM-dd"), ["runs"] = runs, ["recaptcha"] = scores };
Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
```

```go
// go get github.com/playwright-community/playwright-go@v0.6000.0
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY (one session at a time; public test pages only; nothing is solved or retried to get a result)
package main

import (
	"bytes"
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"math"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/playwright-community/playwright-go"
)

var key = os.Getenv("CDPFLEET_API_KEY")

// One sticky residential exit for the whole run, shared by every setup: a difference between rows is the browser
// and how it was driven, not the IP.
var proxy = func() string {
	b := make([]byte, 3)
	rand.Read(b)
	return "cdpfleet-resi-country-us-session-c81" + hex.EncodeToString(b) + "-lifetime-30"
}()

const repeats = 3

type setup struct {
	Key, Name, Engine, Type string
	Body                    map[string]any
}

// Camoufox's humanize option: false = the cursor jumps straight to the target, true (the default) = a curved path of
// up to 1.5 s, a number = a path of up to that many seconds. Chrome is the reference: plain Playwright clicks.
var setups = []setup{
	{"cf_off", "Camoufox, humanize: false", "camoufox", "firefox", map[string]any{"headless": true, "humanize": false}},
	{"cf_on", "Camoufox, humanize: true (default)", "camoufox", "firefox", map[string]any{"headless": true, "humanize": true}},
	{"cf_slow", "Camoufox, humanize: 3", "camoufox", "firefox", map[string]any{"headless": true, "humanize": 3}},
	{"chrome", "Google Chrome, headful", "chrome", "chromium", map[string]any{"headless": false}},
}

// The same actions in every cell; only the typing speed differs between the two form runs of a session.
var typings = []struct {
	Name  string
	Delay float64 // ms between keys
}{{"fast", 0}, {"paced", 120}}

const email = "lab81@example.com"
const password = "lab-81!pass" // the demo form wants 8+ characters with a special character

// deviceandbrowserinfo.com's behavioural bot test: a demo login form that records clicks, keystrokes and timing
// and, on submit, posts them with a fingerprint to its own endpoint, which answers human or bot with its flags.
const formURL = "https://deviceandbrowserinfo.com/are_you_a_bot_interactions"

// Google's public reCAPTCHA v3 demo prints its server's verify response (with the score); its "Try again" link reloads it.
const recaptchaURL = "https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php"

// The same expressions run in every language.
// Count mousemove events on the page, so the table shows what humanize changed.
const countMovesJS = `(() => { window.__lab81 = 0; document.addEventListener('mousemove', () => { window.__lab81++; }, true); return true; })()`

// The verdict the page shows after submit: isBot and every detail flag that is true.
const verdictJS = `(() => {
  const e = document.getElementById('jsonResult');
  const t = e ? e.innerText : '';
  if (t.length < 10) return null;
  const r = JSON.parse(t);
  const d = r.details || {};
  return { is_bot: r.isBot === true, flags: Object.keys(d).filter((k) => d[k] === true), moves: window.__lab81 };
})()`

// What the page itself measured about the interaction: the "interactions" part of the body it posted, which is
// embedded as a JSON string and passed to this function.
const measuredJS = `(body) => {
  const i = JSON.parse(body).interactions || {};
  const s = i.emailTimingStats || {};
  return {
    time_to_fill_ms: Math.round(i.timeToFill),
    exact_center_clicks: [i.hasClickedEmailFieldExactCenter, i.hasClickedPasswordFieldExactCenter, i.hasClickedSubmitExactCenter].filter(Boolean).length,
    untrusted_event: i.hasUntrustedEvent === true,
    cdp_mouse_leak: i.cdpMouseLeak === true,
    key_gap_median_ms: typeof s.medianTimeBetweenKeys === 'number' ? Math.round(s.medianTimeBetweenKeys) : null,
  };
}`

const scoreJS = `(() => { if (window.__lab81rc) return null; const m = /"score"\s*:\s*([0-9.]+)/.exec(document.body ? document.body.innerText : ''); return m ? Number(m[1]) : null; })()`

type measured struct {
	TimeToFillMs      int  `json:"time_to_fill_ms"`
	ExactCenterClicks int  `json:"exact_center_clicks"`
	UntrustedEvent    bool `json:"untrusted_event"`
	CdpMouseLeak      bool `json:"cdp_mouse_leak"`
	KeyGapMedianMs    *int `json:"key_gap_median_ms"`
}
type formRun struct {
	Setup         string    `json:"setup"`
	Key           string    `json:"key"`
	Repeat        int       `json:"repeat"`
	Typing        string    `json:"typing"`
	Verdict       string    `json:"verdict"`
	Flags         []string  `json:"flags"`
	Moves         *int      `json:"moves"`
	ActionSeconds *float64  `json:"action_seconds"`
	Measured      *measured `json:"measured"`
}
type scoreRun struct {
	Setup  string   `json:"setup"`
	Key    string   `json:"key"`
	Repeat int      `json:"repeat"`
	Before *float64 `json:"before"`
	After  *float64 `json:"after"`
}

var pw *playwright.Playwright

func launch(s setup) (playwright.Browser, error) {
	body := map[string]any{"proxy": proxy, "record": false}
	for k, v := range s.Body {
		body[k] = v
	}
	payload, _ := json.Marshal(body)
	for attempt := 1; ; attempt++ {
		req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+s.Engine+"/session", bytes.NewReader(payload))
		req.Header.Set("x-api-key", key)
		req.Header.Set("content-type", "application/json")
		res, err := http.DefaultClient.Do(req)
		if err != nil {
			return nil, err
		}
		msg, _ := io.ReadAll(res.Body)
		res.Body.Close()
		if res.StatusCode == 200 {
			var session struct{ WsURL string }
			json.Unmarshal(msg, &session)
			bt := pw.Chromium
			if s.Type == "firefox" {
				bt = pw.Firefox
			}
			return bt.Connect(session.WsURL, playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
		}
		if (res.StatusCode != 429 && res.StatusCode != 503) || attempt == 5 {
			return nil, errors.New("launch " + strconv.Itoa(res.StatusCode) + " " + string(msg))
		}
		wait, _ := strconv.Atoi(res.Header.Get("retry-after"))
		if wait == 0 {
			wait = 5
		}
		time.Sleep(time.Duration(wait) * time.Second)
	}
}

// A load that fails on the network (proxy timeout) is tried once more; the page's answer is never retried.
func open(tab playwright.Page, url string) bool {
	for attempt := 1; attempt <= 2; attempt++ {
		_, err := tab.Goto(url, playwright.PageGotoOptions{WaitUntil: playwright.WaitUntilStateDomcontentloaded, Timeout: playwright.Float(30000)})
		if err == nil {
			return true
		}
	}
	return false
}

// decode copies what evaluate returned into a typed value.
func decode(v any, into any) bool {
	if v == nil {
		return false
	}
	raw, err := json.Marshal(v)
	return err == nil && json.Unmarshal(raw, into) == nil
}

func score(tab playwright.Page) *float64 {
	v, err := tab.Evaluate(scoreJS)
	if err != nil || v == nil {
		return nil
	}
	var f float64
	if !decode(v, &f) {
		return nil
	}
	return &f
}

// One form run: click the email field, type, click the password field, type, click Login; read the verdict.
func form(browser playwright.Browser, run formRun, delay float64) formRun {
	run.Verdict, run.Flags = "error", []string{}
	ctx, err := browser.NewContext() // a fresh context: every run is a first visit
	if err != nil {
		return run
	}
	defer ctx.Close()
	tab, err := ctx.NewPage()
	if err != nil {
		return run
	}
	var mu sync.Mutex
	posted := ""
	tab.OnRequest(func(req playwright.Request) {
		if strings.Contains(req.URL(), "/fingerprint_bot_test") {
			if body, err := req.PostData(); err == nil {
				mu.Lock()
				posted = body
				mu.Unlock()
			}
		}
	})
	if !open(tab, formURL) {
		return run
	}
	time.Sleep(time.Second)
	if _, err := tab.Evaluate(countMovesJS); err != nil {
		return run
	}
	t0 := time.Now()
	steps := []func() error{
		func() error { return tab.Click("#email") },
		func() error { return tab.Keyboard().Type(email, playwright.KeyboardTypeOptions{Delay: playwright.Float(delay)}) },
		func() error { return tab.Click("#password") },
		func() error { return tab.Keyboard().Type(password, playwright.KeyboardTypeOptions{Delay: playwright.Float(delay)}) },
		func() error { return tab.Click("#loginForm button[type=submit]") },
	}
	for _, step := range steps {
		if err := step(); err != nil {
			return run
		}
	}
	secs := math.Round(time.Since(t0).Seconds()*10) / 10
	run.ActionSeconds = &secs
	var v struct {
		IsBot bool     `json:"is_bot"`
		Flags []string `json:"flags"`
		Moves *int     `json:"moves"`
	}
	got := false
	for i := 0; i < 60 && !got; i++ {
		time.Sleep(500 * time.Millisecond)
		if r, err := tab.Evaluate(verdictJS); err == nil {
			got = decode(r, &v)
		}
	}
	if !got {
		run.Verdict = "no verdict"
		return run
	}
	run.Verdict, run.Flags, run.Moves = "human", v.Flags, v.Moves
	if v.IsBot {
		run.Verdict = "bot"
	}
	mu.Lock()
	body := posted
	mu.Unlock()
	if body != "" {
		arg, _ := json.Marshal(body)
		if r, err := tab.Evaluate("(" + measuredJS + ")(" + string(arg) + ")"); err == nil {
			var m measured
			if decode(r, &m) {
				run.Measured = &m
			}
		}
	}
	return run
}

// Google's v3 demo: wait for the score it asks for on load, move the cursor and click "Try again", read the new score.
func recaptcha(browser playwright.Browser, run scoreRun) scoreRun {
	ctx, err := browser.NewContext()
	if err != nil {
		return run
	}
	defer ctx.Close()
	tab, err := ctx.NewPage()
	if err != nil || !open(tab, recaptchaURL) {
		return run
	}
	for i := 0; i < 40 && run.Before == nil; i++ {
		time.Sleep(500 * time.Millisecond)
		run.Before = score(tab)
	}
	// Marks this document, so the score is read from the reloaded one.
	if _, err := tab.Evaluate("window.__lab81rc = 1"); err != nil {
		return run
	}
	if tab.Mouse().Move(300, 300) != nil || tab.Mouse().Move(600, 200) != nil || tab.Click(`a[href="/recaptcha-v3-request-scores.php"]`) != nil {
		return run
	}
	for i := 0; i < 40 && run.After == nil; i++ {
		time.Sleep(500 * time.Millisecond)
		run.After = score(tab)
	}
	return run
}

func main() {
	var err error
	pw, err = playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
	if err != nil {
		log.Fatal(err)
	}
	defer pw.Stop()
	runs := []formRun{}
	scores := []scoreRun{}
	// Serial: one session at a time; the repeats are interleaved so a slow minute doesn't land on one setup.
	for repeat := 1; repeat <= repeats; repeat++ {
		for _, s := range setups {
			browser, err := launch(s)
			for _, t := range typings {
				run := formRun{Setup: s.Name, Key: s.Key, Repeat: repeat, Typing: t.Name, Verdict: "error", Flags: []string{}}
				if err == nil {
					run = form(browser, run, t.Delay)
				}
				runs = append(runs, run)
			}
			sc := scoreRun{Setup: s.Name, Key: s.Key, Repeat: repeat}
			if err == nil {
				sc = recaptcha(browser, sc)
				browser.Close()
			}
			scores = append(scores, sc)
		}
	}
	out, _ := json.MarshalIndent(map[string]any{"measured_at": time.Now().UTC().Format("2006-01-02"), "runs": runs, "recaptcha": scores}, "", "  ")
	fmt.Println(string(out))
}
```

## What we got

| Setup | Fast typing: verdict | Paced typing: verdict | Time to fill, median (fast · paced) | mousemove events per form, median | Clicks on the exact centre | reCAPTCHA v3 demo after moves + click | Measured |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Camoufox, humanize: false | bot 1/3 (suspiciousClientSideBehavior 1, superHumanSpeed 1) | human 3/3 | 1.9 s · 4.3 s | 3 | 18/18 | 0.9 · 0.9 · 0.9 | 2026-11-04 |
| Camoufox, humanize: true (default) | human 3/3 | human 3/3 | 3.2 s · 5.2 s | 72 | 18/18 | 0.9 · 0.9 · 0.9 | 2026-11-04 |
| Camoufox, humanize: 3 | human 3/3 | human 3/3 | 3.1 s · 5.0 s | 67 | 18/18 | 0.9 · 0.9 · 0.9 | 2026-11-04 |
| Google Chrome, headful | bot 3/3 (hasInconsistentTimingResolution 3, isAutomatedWithCDP 3, isAutomatedWithCDPInWebWorker 3, hasSuspiciousWeakSignals 3, suspiciousClientSideBehavior 3, superHumanSpeed 3) | bot 3/3 (hasInconsistentTimingResolution 3, isAutomatedWithCDP 3, isAutomatedWithCDPInWebWorker 3, hasSuspiciousWeakSignals 3, suspiciousClientSideBehavior 3) | 192 ms · 4.1 s | 3 | 18/18 | 0.9 · none · 0.9 | 2026-11-04 |

From the Node.js run on 2026-11-04. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)

```
{
  "measured_at": "2026-11-04",
  "runs": [
    {
      "setup": "Camoufox, humanize: false",
      "key": "cf_off",
      "repeat": 1,
      "typing": "fast",
      "verdict": "human",
      "flags": [],
      "moves": 3,
      "action_seconds": 2.7,
      "measured": {
        "time_to_fill_ms": 2302,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 16
      }
    },
    {
      "setup": "Camoufox, humanize: false",
      "key": "cf_off",
      "repeat": 1,
      "typing": "paced",
      "verdict": "human",
      "flags": [],
      "moves": 3,
      "action_seconds": 4.5,
      "measured": {
        "time_to_fill_ms": 4254,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 125
      }
    },
    {
      "setup": "Camoufox, humanize: true (default)",
      "key": "cf_on",
      "repeat": 1,
      "typing": "fast",
      "verdict": "human",
      "flags": [],
      "moves": 58,
      "action_seconds": 6.8,
      "measured": {
        "time_to_fill_ms": 4316,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 9
      }
    },
    {
      "setup": "Camoufox, humanize: true (default)",
      "key": "cf_on",
      "repeat": 1,
      "typing": "paced",
      "verdict": "human",
      "flags": [],
      "moves": 84,
      "action_seconds": 6.4,
      "measured": {
        "time_to_fill_ms": 5261,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 126
      }
    },
    {
      "setup": "Camoufox, humanize: 3",
      "key": "cf_slow",
      "repeat": 1,
      "typing": "fast",
      "verdict": "human",
      "flags": [],
      "moves": 78,
      "action_seconds": 2.6,
      "measured": {
        "time_to_fill_ms": 1335,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 3
      }
    },
    {
      "setup": "Camoufox, humanize: 3",
      "key": "cf_slow",
      "repeat": 1,
      "typing": "paced",
      "verdict": "human",
      "flags": [],
      "moves": 49,
      "action_seconds": 5.5,
      "measured": {
        "time_to_fill_ms": 4391,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 123
      }
    },
    {
      "setup": "Google Chrome, headful",
      "key": "chrome",
      "repeat": 1,
      "typing": "fast",
      "verdict": "bot",
      "flags": [
        "hasInconsistentTimingResolution",
        "isAutomatedWithCDP",
        "isAutomatedWithCDPInWebWorker",
        "hasSuspiciousWeakSignals",
        "suspiciousClientSideBehavior",
        "superHumanSpeed"
      ],
      "moves": 3,
      "action_seconds": 0.8,
      "measured": {
        "time_to_fill_ms": 350,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 3
      }
    },
    {
      "setup": "Google Chrome, headful",
      "key": "chrome",
      "repeat": 1,
      "typing": "paced",
      "verdict": "bot",
      "flags": [
        "hasInconsistentTimingResolution",
        "isAutomatedWithCDP",
        "isAutomatedWithCDPInWebWorker",
        "hasSuspiciousWeakSignals",
        "suspiciousClientSideBehavior"
      ],
      "moves": 3,
      "action_seconds": 4.5,
      "measured": {
        "time_to_fill_ms": 4156,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 125
      }
    },
    {
      "setup": "Camoufox, humanize: false",
      "key": "cf_off",
      "repeat": 2,
      "typing": "fast",
      "verdict": "human",
      "flags": [],
      "moves": 3,
      "action_seconds": 2.2,
      "measured": {
        "time_to_fill_ms": 1939,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 4
      }
    },
    {
      "setup": "Camoufox, humanize: false",
      "key": "cf_off",
      "repeat": 2,
      "typing": "paced",
      "verdict": "human",
      "flags": [],
      "moves": 3,
      "action_seconds": 4.2,
      "measured": {
        "time_to_fill_ms": 4016,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 128
      }
    },
    {
      "setup": "Camoufox, humanize: true (default)",
      "key": "cf_on",
      "repeat": 2,
      "typing": "fast",
      "verdict": "human",
      "flags": [],
      "moves": 75,
      "action_seconds": 5.9,
      "measured": {
        "time_to_fill_ms": 3225,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 14
      }
    },
    {
      "setup": "Camoufox, humanize: true (default)",
      "key": "cf_on",
      "repeat": 2,
      "typing": "paced",
      "verdict": "human",
      "flags": [],
      "moves": 72,
      "action_seconds": 6.2,
      "measured": {
        "time_to_fill_ms": 5243,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 127
      }
    },
    {
      "setup": "Camoufox, humanize: 3",
      "key": "cf_slow",
      "repeat": 2,
      "typing": "fast",
      "verdict": "human",
      "flags": [],
      "moves": 67,
      "action_seconds": 6.5,
      "measured": {
        "time_to_fill_ms": 4621,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 32
      }
    },
    {
      "setup": "Camoufox, humanize: 3",
      "key": "cf_slow",
      "repeat": 2,
      "typing": "paced",
      "verdict": "human",
      "flags": [],
      "moves": 76,
      "action_seconds": 6.7,
      "measured": {
        "time_to_fill_ms": 5518,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 129
      }
    },
    {
      "setup": "Google Chrome, headful",
      "key": "chrome",
      "repeat": 2,
      "typing": "fast",
      "verdict": "bot",
      "flags": [
        "hasInconsistentTimingResolution",
        "isAutomatedWithCDP",
        "isAutomatedWithCDPInWebWorker",
        "hasSuspiciousWeakSignals",
        "suspiciousClientSideBehavior",
        "superHumanSpeed"
      ],
      "moves": 3,
      "action_seconds": 0.6,
      "measured": {
        "time_to_fill_ms": 192,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 3
      }
    },
    {
      "setup": "Google Chrome, headful",
      "key": "chrome",
      "repeat": 2,
      "typing": "paced",
      "verdict": "bot",
      "flags": [
        "hasInconsistentTimingResolution",
        "isAutomatedWithCDP",
        "isAutomatedWithCDPInWebWorker",
        "hasSuspiciousWeakSignals",
        "suspiciousClientSideBehavior"
      ],
      "moves": 3,
      "action_seconds": 4.4,
      "measured": {
        "time_to_fill_ms": 4061,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 125
      }
    },
    {
      "setup": "Camoufox, humanize: false",
      "key": "cf_off",
      "repeat": 3,
      "typing": "fast",
      "verdict": "bot",
      "flags": [
        "suspiciousClientSideBehavior",
        "superHumanSpeed"
      ],
      "moves": 3,
      "action_seconds": 0.9,
      "measured": {
        "time_to_fill_ms": 508,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 4
      }
    },
    {
      "setup": "Camoufox, humanize: false",
      "key": "cf_off",
      "repeat": 3,
      "typing": "paced",
      "verdict": "human",
      "flags": [],
      "moves": 3,
      "action_seconds": 4.6,
      "measured": {
        "time_to_fill_ms": 4305,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 126
      }
    },
    {
      "setup": "Camoufox, humanize: true (default)",
      "key": "cf_on",
      "repeat": 3,
      "typing": "fast",
      "verdict": "human",
      "flags": [],
      "moves": 43,
      "action_seconds": 4.2,
      "measured": {
        "time_to_fill_ms": 3067,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 14
      }
    },
    {
      "setup": "Camoufox, humanize: true (default)",
      "key": "cf_on",
      "repeat": 3,
      "typing": "paced",
      "verdict": "human",
      "flags": [],
      "moves": 75,
      "action_seconds": 6.1,
      "measured": {
        "time_to_fill_ms": 4937,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 124
      }
    },
    {
      "setup": "Camoufox, humanize: 3",
      "key": "cf_slow",
      "repeat": 3,
      "typing": "fast",
      "verdict": "human",
      "flags": [],
      "moves": 69,
      "action_seconds": 4.4,
      "measured": {
        "time_to_fill_ms": 3123,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 7
      }
    },
    {
      "setup": "Camoufox, humanize: 3",
      "key": "cf_slow",
      "repeat": 3,
      "typing": "paced",
      "verdict": "human",
      "flags": [],
      "moves": 65,
      "action_seconds": 6.2,
      "measured": {
        "time_to_fill_ms": 5014,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 126
      }
    },
    {
      "setup": "Google Chrome, headful",
      "key": "chrome",
      "repeat": 3,
      "typing": "fast",
      "verdict": "bot",
      "flags": [
        "hasInconsistentTimingResolution",
        "isAutomatedWithCDP",
        "isAutomatedWithCDPInWebWorker",
        "hasSuspiciousWeakSignals",
        "suspiciousClientSideBehavior",
        "superHumanSpeed"
      ],
      "moves": 3,
      "action_seconds": 0.5,
      "measured": {
        "time_to_fill_ms": 171,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 3
      }
    },
    {
      "setup": "Google Chrome, headful",
      "key": "chrome",
      "repeat": 3,
      "typing": "paced",
      "verdict": "bot",
      "flags": [
        "hasInconsistentTimingResolution",
        "isAutomatedWithCDP",
        "isAutomatedWithCDPInWebWorker",
        "hasSuspiciousWeakSignals",
        "suspiciousClientSideBehavior"
      ],
      "moves": 3,
      "action_seconds": 4.2,
      "measured": {
        "time_to_fill_ms": 3958,
        "exact_center_clicks": 3,
        "untrusted_event": false,
        "cdp_mouse_leak": false,
        "key_gap_median_ms": 125
      }
    }
  ],
  "recaptcha": [
    {
      "setup": "Camoufox, humanize: false",
      "key": "cf_off",
      "repeat": 1,
      "before": 0.9,
      "after": 0.9
    },
    {
      "setup": "Camoufox, humanize: true (default)",
      "key": "cf_on",
      "repeat": 1,
      "before": 0.9,
      "after": 0.9
    },
    {
      "setup": "Camoufox, humanize: 3",
      "key": "cf_slow",
      "repeat": 1,
      "before": 0.9,
      "after": 0.9
    },
    {
      "setup": "Google Chrome, headful",
      "key": "chrome",
      "repeat": 1,
      "before": 0.9,
      "after": 0.9
    },
    {
      "setup": "Camoufox, humanize: false",
      "key": "cf_off",
      "repeat": 2,
      "before": 0.9,
      "after": 0.9
    },
    {
      "setup": "Camoufox, humanize: true (default)",
      "key": "cf_on",
      "repeat": 2,
      "before": 0.9,
      "after": 0.9
    },
    {
      "setup": "Camoufox, humanize: 3",
      "key": "cf_slow",
      "repeat": 2,
      "before": 0.9,
      "after": 0.9
    },
    {
      "setup": "Google Chrome, headful",
      "key": "chrome",
      "repeat": 2,
      "before": null,
      "after": null
    },
    {
      "setup": "Camoufox, humanize: false",
      "key": "cf_off",
      "repeat": 3,
      "before": 0.9,
      "after": 0.9
    },
    {
      "setup": "Camoufox, humanize: true (default)",
      "key": "cf_on",
      "repeat": 3,
      "before": 0.9,
      "after": 0.9
    },
    {
      "setup": "Camoufox, humanize: 3",
      "key": "cf_slow",
      "repeat": 3,
      "before": 0.9,
      "after": 0.9
    },
    {
      "setup": "Google Chrome, headful",
      "key": "chrome",
      "repeat": 3,
      "before": 0.9,
      "after": 0.9
    }
  ]
}
```

## Takeaways

-   **Plain Playwright in headful Chrome was a bot in all 30 runs, and paced typing didn't change that.** On 2026-11-04 the page raised `isAutomatedWithCDP` (and its web-worker variant) every time, which is about the browser being driven over CDP, not about the cursor; `suspiciousClientSideBehavior` came with it in all 30 runs, paced or not. `superHumanSpeed` appeared in all 15 fast runs (form filled in a median 0.3 s) and in none of the 15 paced ones (about 4 s).
-   **`humanize` changed how the cursor travelled, not where it clicked.** With `humanize: false` the page saw 3 `mousemove` events per form, one per click; with `humanize: true` it saw 43–86 and with `humanize: 3` 49–90. Every click in every setup (354 of 354 measured) still landed within 1 px of the exact centre of its target, because Playwright aims at the centre and `humanize` only shapes the path. The page records centre clicks; on their own they didn't make it say "bot". No run had an untrusted event or tripped its CDP mouse-coordinate check.
-   **With paced typing, Camoufox raised no behaviour flag in 44 of 44 runs — `humanize` off, on or 3.** The verdict was "human" in 43; the one "bot" (humanize on) came from a single fingerprint flag, `hasInconsistentTimingResolution`, with no behaviour flag at all.
-   **What the page flagged in Camoufox was speed.** With fast typing, `humanize: false` got `suspiciousClientSideBehavior` + `superHumanSpeed` in 5 of 14 runs; `humanize: true` and `humanize: 3` in 0 of 15 each. Every flagged run had filled the form in under 0.7 s (time from the first field click to submit, as the page measures it); no run that took 1.5 s or longer was flagged. `humanize` doesn't change typing, but its cursor travel made filling the form slower: median 1.4 s off, 2.6 s with `humanize: true` and 2.6 s with `3` (fastest humanized run: 1.0 s).
-   **Google's reCAPTCHA v3 demo still said 0.9 after real cursor moves and a click.** 56 of 60 sessions got a score after the reload, every one 0.9, for all four setups, the same as the score each got before moving (55 of 60, all 0.9) and as [case 68](https://cdpfleet.com/docs/cases/captcha-scores-by-browser) with nothing touched. The demo doesn't separate these setups; a page built to score behaviour did.
-   **A demo page's verdict is not a real site's.** This page scores one form with fixed rules, says it ignores IP reputation, and lets you repeat the test. Real protections weigh far more — see the lab cases from [Cloudflare](https://cdpfleet.com/docs/cases/cloudflare-what-gets-through) to [Kasada and AWS WAF](https://cdpfleet.com/docs/cases/kasada-and-aws-waf-by-browser) and the [scoreboard](https://cdpfleet.com/bot-protection/scoreboard).

---
Source: https://cdpfleet.com/docs/cases/behaviour-scoring-humanize · Markdown: https://cdpfleet.com/docs/cases/behaviour-scoring-humanize.md · All docs for AI tools: https://cdpfleet.com/llms.txt
