Cases / #62 · 2026-10-20 · Easy
Which bot protection is this? Detect the vendor from cookies, headers and requests
Load 20 public pages once each (the vendors' own demo pages plus large public sites) and name the bot-protection vendor from the signals it leaves: response headers, cookie names and the hosts the page calls. Counts and signals per vendor, with the outcome each page showed on the measurement date.
Run on production on 2026-10-20: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Before you tune a browser, a proxy or a wait for a page that won't load, find out what is in front of it. "Blocked" means different things behind Cloudflare, DataDome, Akamai, HUMAN or Kasada, and each one is recognisable from the outside: it sets cookies with known names, adds response headers, and loads its scripts from its own hosts. A detector that reads those signals from one ordinary page load tells you which vendor you are dealing with, and which of the later lab cases applies. This case only observes: it loads public pages once, solves nothing and retries nothing.
What we used, and why
| What | Why |
|---|---|
response.allHeaders() on the main document | Vendor headers: cf-ray (Cloudflare), x-datadome (DataDome), akamai-* (Akamai), x-kpsdk-* (Kasada), x-iinfo (Imperva). |
context.cookies() after 6 s | Cookie names the protection scripts set: __cf_bm/cf_clearance, datadome, _abck/bm_sz, _px*, incap_ses_*/visid_incap_*, aws-waf-token. Only the names are read. |
request events | Every URL the page loads: /cdn-cgi/challenge-platform, the Turnstile widget on challenges.cloudflare.com, captcha-delivery.com, HUMAN's px-cdn.net/px-cloud.net, Kasada's ips.js, awswaf.com, reCAPTCHA and hCaptcha. |
| Title and the first lines of text | The outcome in the lab's words: content, challenge (an interactive CAPTCHA or "press and hold"), blocked (a block page or a short 403/429), or error. |
| A fresh context per page | No cookies carried from one site to the next, so each page is a first visit. |
| One baseline setup | Headless Chromium on cdpfleet through your own proxy (PROXY_URL). The cases later this week compare browsers and networks on the same vendors. |
How it works
- For each of 20 public pages, home or demo pages only: the vendors' own test pages (nowsecure.nl, scrapingcourse.com's Cloudflare challenge, peet.ws's Turnstile test, Google's reCAPTCHA v3 demo, hCaptcha's demo) and two or three large public sites per vendor, open a fresh context and load it once.
- Record every request URL; after DOM ready read the main response's headers, wait 6 s for the protection scripts, then read the title, the opening text and the cookie names.
- Match the signals for each vendor, classify the outcome, and add the page to every vendor found. Print counts per vendor and signal, not a list of sites; only the demo pages are named.
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
CDPFLEET_API_KEY— your API key (dashboard → API keys)PROXY_URL— your proxy, e.g.http://user:[email protected]:8000
// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
// Public pages, each loaded once. `demo` pages are the protection vendors' own test pages and are
// reported by name; for the rest only the counts per vendor are reported. `expect` is the vendor a
// first survey (2026-10-19) found on that page.
const PAGES = [
{ url: 'https://nowsecure.nl/', expect: 'turnstile', demo: 'nowsecure.nl (Turnstile test page)' },
{ url: 'https://www.scrapingcourse.com/cloudflare-challenge', expect: 'cloudflare', demo: 'scrapingcourse.com Cloudflare challenge' },
{ url: 'https://peet.ws/turnstile-test/non-interactive.html', expect: 'turnstile', demo: 'peet.ws Turnstile test (non-interactive)' },
{ url: 'https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php', expect: 'recaptcha', demo: 'Google reCAPTCHA v3 demo' },
{ url: 'https://accounts.hcaptcha.com/demo', expect: 'hcaptcha', demo: 'hCaptcha demo' },
{ url: 'https://www.crunchbase.com/', expect: 'cloudflare' },
{ url: 'https://discord.com/', expect: 'cloudflare' },
{ url: 'https://www.glassdoor.com/', expect: 'cloudflare' },
{ url: 'https://www.tripadvisor.com/', expect: 'datadome' },
{ url: 'https://www.etsy.com/', expect: 'datadome' },
{ url: 'https://soundcloud.com/', expect: 'datadome' },
{ url: 'https://www.homedepot.com/', expect: 'akamai' },
{ url: 'https://www.usps.com/', expect: 'akamai' },
{ url: 'https://www.delta.com/', expect: 'akamai' },
{ url: 'https://www.zillow.com/', expect: 'human' },
{ url: 'https://www.walmart.com/', expect: 'human' },
{ url: 'https://www.realestate.com.au/', expect: 'kasada' },
{ url: 'https://www.ticketmaster.com/', expect: 'kasada' },
{ url: 'https://www.amazon.com/', expect: 'awswaf' },
{ url: 'https://www.booking.com/', expect: 'awswaf' },
];
const VENDORS = ['cloudflare', 'turnstile', 'datadome', 'akamai', 'human', 'kasada', 'imperva', 'awswaf', 'recaptcha', 'hcaptcha'];
// [vendor, signal label, test] — each vendor leaves response headers, cookies or requests to its own hosts.
const host = (u) => { try { return new URL(u).hostname; } catch { return ''; } };
const SIGNALS = [
['cloudflare', 'cf-ray header', ({ h }) => 'cf-ray' in h],
['cloudflare', '__cf_bm / cf_clearance cookie', ({ cookies }) => cookies.some((c) => ['__cf_bm', 'cf_clearance', '__cflb'].includes(c))],
['cloudflare', '/cdn-cgi/challenge-platform request', ({ urls }) => urls.some((u) => u.includes('/cdn-cgi/challenge-platform'))],
['turnstile', 'Turnstile widget', ({ urls }) => urls.some((u) => u.includes('challenges.cloudflare.com') && u.includes('turnstile'))],
['datadome', 'x-datadome header', ({ h }) => 'x-datadome' in h || 'x-dd-b' in h],
['datadome', 'datadome cookie', ({ cookies }) => cookies.includes('datadome')],
['datadome', 'DataDome host (captcha-delivery.com, datadome.co)', ({ urls }) => urls.some((u) => /captcha-delivery\.com|datadome\.co/.test(host(u)))],
['akamai', '_abck / bm_sz cookie', ({ cookies }) => cookies.some((c) => ['_abck', 'bm_sz', 'ak_bmsc', 'bm_sv'].includes(c))],
['akamai', 'akamai header', ({ h }) => Object.keys(h).some((k) => k.startsWith('akamai') || k.startsWith('x-akamai'))],
['human', '_px cookie', ({ cookies }) => cookies.some((c) => c.startsWith('_px'))],
['human', 'HUMAN host (px-cdn.net, px-cloud.net)', ({ urls }) => urls.some((u) => /px-cdn\.net|px-cloud\.net|perimeterx\.net|pxchk\.net/.test(host(u)))],
['kasada', 'x-kpsdk header', ({ h }) => Object.keys(h).some((k) => k.startsWith('x-kpsdk'))],
['kasada', 'ips.js / kpsdk request', ({ urls }) => urls.some((u) => u.includes('/ips.js') || u.includes('kpsdk'))],
['imperva', 'x-iinfo header', ({ h }) => 'x-iinfo' in h],
['imperva', 'incap_ses / visid_incap cookie', ({ cookies }) => cookies.some((c) => /^(incap_ses|visid_incap|nlbi_)/.test(c))],
['awswaf', 'aws-waf-token cookie', ({ cookies }) => cookies.includes('aws-waf-token')],
['awswaf', 'AWS WAF host (awswaf.com)', ({ urls }) => urls.some((u) => /awswaf\.com/.test(host(u)))],
['recaptcha', 'reCAPTCHA script', ({ urls }) => urls.some((u) => u.includes('google.com/recaptcha') || u.includes('recaptcha.net'))],
['hcaptcha', 'hCaptcha host', ({ urls }) => urls.some((u) => /hcaptcha\.com/.test(host(u)))],
];
const CHALLENGE = /just a moment|checking your browser|verify you are human|press & hold|press and hold|are you a robot|human touch/i;
const BLOCKED = /access denied|access to this page has been denied|attention required|you have been blocked|request blocked|error 1020|pardon our interruption/i;
const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: true }),
});
if (!res.ok) throw new Error(`launch: ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
// One load per page in a fresh context: DOM ready + 6 s for the protection scripts to run, then
// read the main response's headers, the cookies and every requested URL.
async function survey(page) {
const context = await browser.newContext();
const tab = await context.newPage();
const urls = [];
tab.on('request', (r) => urls.push(r.url()));
let h = {}, status = 0, outcome;
try {
const r = await tab.goto(page.url, { waitUntil: 'domcontentloaded', timeout: 45000 });
status = r ? r.status() : 0;
h = r ? await r.allHeaders() : {};
await tab.waitForTimeout(6000);
const title = await tab.title().catch(() => '');
const text = (await tab.locator('body').innerText().catch(() => '')).replace(/\s+/g, ' ');
const frames = tab.frames().map((f) => f.url()).join(' ');
if (CHALLENGE.test(`${title} ${text.slice(0, 800)}`) || frames.includes('captcha-delivery.com')) outcome = 'challenge';
else if (BLOCKED.test(`${title} ${text.slice(0, 400)}`) || ([403, 429, 444].includes(status) && text.length < 1500)) outcome = 'blocked';
else outcome = 'content';
} catch {
outcome = 'error';
}
const cookies = (await context.cookies()).map((c) => c.name);
await context.close();
const hits = SIGNALS.filter(([, , test]) => test({ h, cookies, urls }));
return { status, outcome, vendors: [...new Set(hits.map(([v]) => v))], signals: hits.map(([v, label]) => [v, label]) };
}
try {
const vendors = Object.fromEntries(VENDORS.map((v) => [v, { vendor: v, pages: 0, signals: {}, outcomes: { content: 0, challenge: 0, blocked: 0, error: 0 } }]));
const demoPages = [];
let none = 0;
let expectedMatched = 0;
for (const page of PAGES) {
const r = await survey(page);
if (!r.vendors.length) none++;
if (r.vendors.includes(page.expect)) expectedMatched++;
for (const v of r.vendors) {
vendors[v].pages++;
vendors[v].outcomes[r.outcome]++;
}
for (const [v, label] of r.signals) vendors[v].signals[label] = (vendors[v].signals[label] || 0) + 1;
if (page.demo) demoPages.push({ page: page.demo, vendors: r.vendors, outcome: r.outcome });
}
console.log(JSON.stringify({
measured_at: new Date().toISOString().slice(0, 10),
pages: PAGES.length,
no_vendor_found: none,
expected_vendor_found: expectedMatched,
vendors: VENDORS.map((v) => vendors[v]).filter((v) => v.pages > 0),
demo_pages: demoPages,
}, null, 2));
} finally {
await browser.close();
}
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import datetime
import json
import os
import re
from urllib.parse import urlparse
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
# Public pages, each loaded once. `demo` pages are the protection vendors' own test pages and are
# reported by name; for the rest only the counts per vendor are reported. `expect` is the vendor a
# first survey (2026-10-19) found on that page.
PAGES = [
{"url": "https://nowsecure.nl/", "expect": "turnstile", "demo": "nowsecure.nl (Turnstile test page)"},
{"url": "https://www.scrapingcourse.com/cloudflare-challenge", "expect": "cloudflare", "demo": "scrapingcourse.com Cloudflare challenge"},
{"url": "https://peet.ws/turnstile-test/non-interactive.html", "expect": "turnstile", "demo": "peet.ws Turnstile test (non-interactive)"},
{"url": "https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php", "expect": "recaptcha", "demo": "Google reCAPTCHA v3 demo"},
{"url": "https://accounts.hcaptcha.com/demo", "expect": "hcaptcha", "demo": "hCaptcha demo"},
{"url": "https://www.crunchbase.com/", "expect": "cloudflare"},
{"url": "https://discord.com/", "expect": "cloudflare"},
{"url": "https://www.glassdoor.com/", "expect": "cloudflare"},
{"url": "https://www.tripadvisor.com/", "expect": "datadome"},
{"url": "https://www.etsy.com/", "expect": "datadome"},
{"url": "https://soundcloud.com/", "expect": "datadome"},
{"url": "https://www.homedepot.com/", "expect": "akamai"},
{"url": "https://www.usps.com/", "expect": "akamai"},
{"url": "https://www.delta.com/", "expect": "akamai"},
{"url": "https://www.zillow.com/", "expect": "human"},
{"url": "https://www.walmart.com/", "expect": "human"},
{"url": "https://www.realestate.com.au/", "expect": "kasada"},
{"url": "https://www.ticketmaster.com/", "expect": "kasada"},
{"url": "https://www.amazon.com/", "expect": "awswaf"},
{"url": "https://www.booking.com/", "expect": "awswaf"},
]
VENDORS = ["cloudflare", "turnstile", "datadome", "akamai", "human", "kasada", "imperva", "awswaf", "recaptcha", "hcaptcha"]
def host(u):
try:
return urlparse(u).hostname or ""
except ValueError:
return ""
# (vendor, signal label, test) — each vendor leaves response headers, cookies or requests to its own hosts.
SIGNALS = [
("cloudflare", "cf-ray header", lambda h, c, u: "cf-ray" in h),
("cloudflare", "__cf_bm / cf_clearance cookie", lambda h, c, u: any(x in ("__cf_bm", "cf_clearance", "__cflb") for x in c)),
("cloudflare", "/cdn-cgi/challenge-platform request", lambda h, c, u: any("/cdn-cgi/challenge-platform" in x for x in u)),
("turnstile", "Turnstile widget", lambda h, c, u: any("challenges.cloudflare.com" in x and "turnstile" in x for x in u)),
("datadome", "x-datadome header", lambda h, c, u: "x-datadome" in h or "x-dd-b" in h),
("datadome", "datadome cookie", lambda h, c, u: "datadome" in c),
("datadome", "DataDome host (captcha-delivery.com, datadome.co)", lambda h, c, u: any(re.search(r"captcha-delivery\.com|datadome\.co", host(x)) for x in u)),
("akamai", "_abck / bm_sz cookie", lambda h, c, u: any(x in ("_abck", "bm_sz", "ak_bmsc", "bm_sv") for x in c)),
("akamai", "akamai header", lambda h, c, u: any(k.startswith("akamai") or k.startswith("x-akamai") for k in h)),
("human", "_px cookie", lambda h, c, u: any(x.startswith("_px") for x in c)),
("human", "HUMAN host (px-cdn.net, px-cloud.net)", lambda h, c, u: any(re.search(r"px-cdn\.net|px-cloud\.net|perimeterx\.net|pxchk\.net", host(x)) for x in u)),
("kasada", "x-kpsdk header", lambda h, c, u: any(k.startswith("x-kpsdk") for k in h)),
("kasada", "ips.js / kpsdk request", lambda h, c, u: any("/ips.js" in x or "kpsdk" in x for x in u)),
("imperva", "x-iinfo header", lambda h, c, u: "x-iinfo" in h),
("imperva", "incap_ses / visid_incap cookie", lambda h, c, u: any(re.match(r"^(incap_ses|visid_incap|nlbi_)", x) for x in c)),
("awswaf", "aws-waf-token cookie", lambda h, c, u: "aws-waf-token" in c),
("awswaf", "AWS WAF host (awswaf.com)", lambda h, c, u: any(re.search(r"awswaf\.com", host(x)) for x in u)),
("recaptcha", "reCAPTCHA script", lambda h, c, u: any("google.com/recaptcha" in x or "recaptcha.net" in x for x in u)),
("hcaptcha", "hCaptcha host", lambda h, c, u: any(re.search(r"hcaptcha\.com", host(x)) for x in u)),
]
CHALLENGE = re.compile(r"just a moment|checking your browser|verify you are human|press & hold|press and hold|are you a robot|human touch", re.I)
BLOCKED = re.compile(r"access denied|access to this page has been denied|attention required|you have been blocked|request blocked|error 1020|pardon our interruption", re.I)
res = requests.post(
"https://starter.cdpfleet.com/chromium/session",
headers={"x-api-key": KEY},
json={"proxy": os.environ["PROXY_URL"], "headless": True},
timeout=120,
)
res.raise_for_status()
ws_url = res.json()["wsUrl"]
def survey(browser, page):
"""One load in a fresh context: DOM ready + 6 s, then the main response's headers, the cookies, every URL."""
context = browser.new_context()
tab = context.new_page()
urls = []
tab.on("request", lambda r: urls.append(r.url))
h, status = {}, 0
try:
r = tab.goto(page["url"], wait_until="domcontentloaded", timeout=45000)
status = r.status if r else 0
h = r.all_headers() if r else {}
tab.wait_for_timeout(6000)
title = tab.title()
try:
text = re.sub(r"\s+", " ", tab.locator("body").inner_text())
except Exception:
text = ""
frames = " ".join(f.url for f in tab.frames)
if CHALLENGE.search(f"{title} {text[:800]}") or "captcha-delivery.com" in frames:
outcome = "challenge"
elif BLOCKED.search(f"{title} {text[:400]}") or (status in (403, 429, 444) and len(text) < 1500):
outcome = "blocked"
else:
outcome = "content"
except Exception:
outcome = "error"
cookies = [c["name"] for c in context.cookies()]
context.close()
hits = [(v, label) for v, label, test in SIGNALS if test(h, cookies, urls)]
found = list(dict.fromkeys(v for v, _ in hits))
return {"status": status, "outcome": outcome, "vendors": found, "signals": hits}
with sync_playwright() as p:
browser = p.chromium.connect(ws_url, headers={"x-api-key": KEY})
try:
vendors = {v: {"vendor": v, "pages": 0, "signals": {}, "outcomes": {"content": 0, "challenge": 0, "blocked": 0, "error": 0}} for v in VENDORS}
demo_pages, none, expected_matched = [], 0, 0
for page in PAGES:
r = survey(browser, page)
if not r["vendors"]:
none += 1
if page["expect"] in r["vendors"]:
expected_matched += 1
for v in r["vendors"]:
vendors[v]["pages"] += 1
vendors[v]["outcomes"][r["outcome"]] += 1
for v, label in r["signals"]:
vendors[v]["signals"][label] = vendors[v]["signals"].get(label, 0) + 1
if page.get("demo"):
demo_pages.append({"page": page["demo"], "vendors": r["vendors"], "outcome": r["outcome"]})
print(json.dumps({
"measured_at": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%d"),
"pages": len(PAGES),
"no_vendor_found": none,
"expected_vendor_found": expected_matched,
"vendors": [vendors[v] for v in VENDORS if vendors[v]["pages"] > 0],
"demo_pages": demo_pages,
}, indent=2))
finally:
browser.close()
// 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, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.*;
import java.net.URI;
import java.net.http.*;
import java.time.*;
import java.util.*;
import java.util.regex.*;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
// Public pages, each loaded once: {url, expected vendor, demo name or null}. Demo pages are the
// protection vendors' own test pages and are reported by name; for the rest only the counts per
// vendor are reported. The expected vendor is what a first survey (2026-10-19) found.
static final String[][] PAGES = {
{"https://nowsecure.nl/", "turnstile", "nowsecure.nl (Turnstile test page)"},
{"https://www.scrapingcourse.com/cloudflare-challenge", "cloudflare", "scrapingcourse.com Cloudflare challenge"},
{"https://peet.ws/turnstile-test/non-interactive.html", "turnstile", "peet.ws Turnstile test (non-interactive)"},
{"https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php", "recaptcha", "Google reCAPTCHA v3 demo"},
{"https://accounts.hcaptcha.com/demo", "hcaptcha", "hCaptcha demo"},
{"https://www.crunchbase.com/", "cloudflare", null},
{"https://discord.com/", "cloudflare", null},
{"https://www.glassdoor.com/", "cloudflare", null},
{"https://www.tripadvisor.com/", "datadome", null},
{"https://www.etsy.com/", "datadome", null},
{"https://soundcloud.com/", "datadome", null},
{"https://www.homedepot.com/", "akamai", null},
{"https://www.usps.com/", "akamai", null},
{"https://www.delta.com/", "akamai", null},
{"https://www.zillow.com/", "human", null},
{"https://www.walmart.com/", "human", null},
{"https://www.realestate.com.au/", "kasada", null},
{"https://www.ticketmaster.com/", "kasada", null},
{"https://www.amazon.com/", "awswaf", null},
{"https://www.booking.com/", "awswaf", null}};
static final List<String> VENDORS = List.of("cloudflare", "turnstile", "datadome", "akamai", "human", "kasada", "imperva", "awswaf", "recaptcha", "hcaptcha");
interface Test { boolean check(Map<String, String> h, List<String> cookies, List<String> urls); }
record Signal(String vendor, String label, Test test) {}
// A lenient host match: URI.create() rejects the unencoded characters some pages put in URLs.
static final Pattern HOST = Pattern.compile("^[a-z]+://([^/:?#]+)");
static String host(String u) { Matcher m = HOST.matcher(u); return m.find() ? m.group(1).toLowerCase() : ""; }
static boolean anyHost(List<String> urls, String re) { Pattern p = Pattern.compile(re); return urls.stream().anyMatch(u -> p.matcher(host(u)).find()); }
// Each vendor leaves response headers, cookies or requests to its own hosts.
static final List<Signal> SIGNALS = List.of(
new Signal("cloudflare", "cf-ray header", (h, c, u) -> h.containsKey("cf-ray")),
new Signal("cloudflare", "__cf_bm / cf_clearance cookie", (h, c, u) -> c.stream().anyMatch(x -> List.of("__cf_bm", "cf_clearance", "__cflb").contains(x))),
new Signal("cloudflare", "/cdn-cgi/challenge-platform request", (h, c, u) -> u.stream().anyMatch(x -> x.contains("/cdn-cgi/challenge-platform"))),
new Signal("turnstile", "Turnstile widget", (h, c, u) -> u.stream().anyMatch(x -> x.contains("challenges.cloudflare.com") && x.contains("turnstile"))),
new Signal("datadome", "x-datadome header", (h, c, u) -> h.containsKey("x-datadome") || h.containsKey("x-dd-b")),
new Signal("datadome", "datadome cookie", (h, c, u) -> c.contains("datadome")),
new Signal("datadome", "DataDome host (captcha-delivery.com, datadome.co)", (h, c, u) -> anyHost(u, "captcha-delivery\\.com|datadome\\.co")),
new Signal("akamai", "_abck / bm_sz cookie", (h, c, u) -> c.stream().anyMatch(x -> List.of("_abck", "bm_sz", "ak_bmsc", "bm_sv").contains(x))),
new Signal("akamai", "akamai header", (h, c, u) -> h.keySet().stream().anyMatch(k -> k.startsWith("akamai") || k.startsWith("x-akamai"))),
new Signal("human", "_px cookie", (h, c, u) -> c.stream().anyMatch(x -> x.startsWith("_px"))),
new Signal("human", "HUMAN host (px-cdn.net, px-cloud.net)", (h, c, u) -> anyHost(u, "px-cdn\\.net|px-cloud\\.net|perimeterx\\.net|pxchk\\.net")),
new Signal("kasada", "x-kpsdk header", (h, c, u) -> h.keySet().stream().anyMatch(k -> k.startsWith("x-kpsdk"))),
new Signal("kasada", "ips.js / kpsdk request", (h, c, u) -> u.stream().anyMatch(x -> x.contains("/ips.js") || x.contains("kpsdk"))),
new Signal("imperva", "x-iinfo header", (h, c, u) -> h.containsKey("x-iinfo")),
new Signal("imperva", "incap_ses / visid_incap cookie", (h, c, u) -> c.stream().anyMatch(x -> x.matches("^(incap_ses|visid_incap|nlbi_).*"))),
new Signal("awswaf", "aws-waf-token cookie", (h, c, u) -> c.contains("aws-waf-token")),
new Signal("awswaf", "AWS WAF host (awswaf.com)", (h, c, u) -> anyHost(u, "awswaf\\.com")),
new Signal("recaptcha", "reCAPTCHA script", (h, c, u) -> u.stream().anyMatch(x -> x.contains("google.com/recaptcha") || x.contains("recaptcha.net"))),
new Signal("hcaptcha", "hCaptcha host", (h, c, u) -> anyHost(u, "hcaptcha\\.com")));
static final Pattern CHALLENGE = Pattern.compile("just a moment|checking your browser|verify you are human|press & hold|press and hold|are you a robot|human touch", Pattern.CASE_INSENSITIVE);
static final Pattern BLOCKED = Pattern.compile("access denied|access to this page has been denied|attention required|you have been blocked|request blocked|error 1020|pardon our interruption", Pattern.CASE_INSENSITIVE);
// One load in a fresh context: DOM ready + 6 s for the protection scripts to run, then the main
// response's headers, the cookies and every requested URL.
static Object[] survey(Browser browser, String url) {
BrowserContext context = browser.newContext();
Page tab = context.newPage();
List<String> urls = Collections.synchronizedList(new ArrayList<>());
tab.onRequest(r -> urls.add(r.url()));
Map<String, String> h = new HashMap<>();
String outcome;
try {
Response r = tab.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED).setTimeout(45000));
int status = r != null ? r.status() : 0;
if (r != null) h = r.allHeaders();
tab.waitForTimeout(6000);
String title = tab.title();
String text;
try { text = tab.locator("body").innerText().replaceAll("\\s+", " "); } catch (PlaywrightException e) { text = ""; }
StringBuilder frames = new StringBuilder();
for (Frame f : tab.frames()) frames.append(f.url()).append(' ');
if (CHALLENGE.matcher(title + " " + text.substring(0, Math.min(800, text.length()))).find() || frames.toString().contains("captcha-delivery.com")) outcome = "challenge";
else if (BLOCKED.matcher(title + " " + text.substring(0, Math.min(400, text.length()))).find() || (List.of(403, 429, 444).contains(status) && text.length() < 1500)) outcome = "blocked";
else outcome = "content";
} catch (PlaywrightException e) {
outcome = "error";
}
List<String> cookies = new ArrayList<>();
for (Cookie c : context.cookies()) cookies.add(c.name);
context.close();
List<String> snapshot;
synchronized (urls) { snapshot = new ArrayList<>(urls); }
List<Signal> hits = new ArrayList<>();
for (Signal s : SIGNALS) if (s.test().check(h, cookies, snapshot)) hits.add(s);
return new Object[] {outcome, hits};
}
public static void main(String[] args) throws Exception {
String body = "{\"proxy\": " + new Gson().toJson(System.getenv("PROXY_URL")) + ", \"headless\": true}";
HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
Map<String, JsonObject> vendors = new LinkedHashMap<>();
for (String v : VENDORS) {
JsonObject o = new JsonObject();
o.addProperty("vendor", v);
o.addProperty("pages", 0);
o.add("signals", new JsonObject());
JsonObject oc = new JsonObject();
for (String k : List.of("content", "challenge", "blocked", "error")) oc.addProperty(k, 0);
o.add("outcomes", oc);
vendors.put(v, o);
}
JsonArray demoPages = new JsonArray();
int none = 0, expectedMatched = 0;
for (String[] page : PAGES) {
Object[] r = survey(browser, page[0]);
String outcome = (String) r[0];
@SuppressWarnings("unchecked") List<Signal> hits = (List<Signal>) r[1];
LinkedHashSet<String> found = new LinkedHashSet<>();
for (Signal s : hits) found.add(s.vendor());
if (found.isEmpty()) none++;
if (found.contains(page[1])) expectedMatched++;
for (String v : found) {
JsonObject o = vendors.get(v);
o.addProperty("pages", o.get("pages").getAsInt() + 1);
JsonObject oc = o.getAsJsonObject("outcomes");
oc.addProperty(outcome, oc.get(outcome).getAsInt() + 1);
}
for (Signal s : hits) {
JsonObject sig = vendors.get(s.vendor()).getAsJsonObject("signals");
sig.addProperty(s.label(), (sig.has(s.label()) ? sig.get(s.label()).getAsInt() : 0) + 1);
}
if (page[2] != null) {
JsonObject d = new JsonObject();
d.addProperty("page", page[2]);
JsonArray vs = new JsonArray();
for (String v : found) vs.add(v);
d.add("vendors", vs);
d.addProperty("outcome", outcome);
demoPages.add(d);
}
}
JsonObject out = new JsonObject();
out.addProperty("measured_at", LocalDate.now(ZoneOffset.UTC).toString());
out.addProperty("pages", PAGES.length);
out.addProperty("no_vendor_found", none);
out.addProperty("expected_vendor_found", expectedMatched);
JsonArray vs = new JsonArray();
for (JsonObject o : vendors.values()) if (o.get("pages").getAsInt() > 0) vs.add(o);
out.add("vendors", vs);
out.add("demo_pages", demoPages);
System.out.println(new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
} finally {
browser.close();
}
}
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.Playwright;
// Public pages, each loaded once: (url, expected vendor, demo name or null). Demo pages are the
// protection vendors' own test pages and are reported by name; for the rest only the counts per
// vendor are reported. The expected vendor is what a first survey (2026-10-19) found.
var pages = new (string Url, string Expect, string? Demo)[]
{
("https://nowsecure.nl/", "turnstile", "nowsecure.nl (Turnstile test page)"),
("https://www.scrapingcourse.com/cloudflare-challenge", "cloudflare", "scrapingcourse.com Cloudflare challenge"),
("https://peet.ws/turnstile-test/non-interactive.html", "turnstile", "peet.ws Turnstile test (non-interactive)"),
("https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php", "recaptcha", "Google reCAPTCHA v3 demo"),
("https://accounts.hcaptcha.com/demo", "hcaptcha", "hCaptcha demo"),
("https://www.crunchbase.com/", "cloudflare", null),
("https://discord.com/", "cloudflare", null),
("https://www.glassdoor.com/", "cloudflare", null),
("https://www.tripadvisor.com/", "datadome", null),
("https://www.etsy.com/", "datadome", null),
("https://soundcloud.com/", "datadome", null),
("https://www.homedepot.com/", "akamai", null),
("https://www.usps.com/", "akamai", null),
("https://www.delta.com/", "akamai", null),
("https://www.zillow.com/", "human", null),
("https://www.walmart.com/", "human", null),
("https://www.realestate.com.au/", "kasada", null),
("https://www.ticketmaster.com/", "kasada", null),
("https://www.amazon.com/", "awswaf", null),
("https://www.booking.com/", "awswaf", null),
};
string[] vendorOrder = { "cloudflare", "turnstile", "datadome", "akamai", "human", "kasada", "imperva", "awswaf", "recaptcha", "hcaptcha" };
// A lenient host match: new Uri() rejects the unencoded characters some pages put in URLs.
string Host(string u) { var m = Regex.Match(u, "^[a-z]+://([^/:?#]+)"); return m.Success ? m.Groups[1].Value.ToLowerInvariant() : ""; }
bool AnyHost(List<string> urls, string re) => urls.Any(u => Regex.IsMatch(Host(u), re));
// Each vendor leaves response headers, cookies or requests to its own hosts.
var signals = new (string Vendor, string Label, Func<Dictionary<string, string>, List<string>, List<string>, bool> Test)[]
{
("cloudflare", "cf-ray header", (h, c, u) => h.ContainsKey("cf-ray")),
("cloudflare", "__cf_bm / cf_clearance cookie", (h, c, u) => c.Any(x => x is "__cf_bm" or "cf_clearance" or "__cflb")),
("cloudflare", "/cdn-cgi/challenge-platform request", (h, c, u) => u.Any(x => x.Contains("/cdn-cgi/challenge-platform"))),
("turnstile", "Turnstile widget", (h, c, u) => u.Any(x => x.Contains("challenges.cloudflare.com") && x.Contains("turnstile"))),
("datadome", "x-datadome header", (h, c, u) => h.ContainsKey("x-datadome") || h.ContainsKey("x-dd-b")),
("datadome", "datadome cookie", (h, c, u) => c.Contains("datadome")),
("datadome", "DataDome host (captcha-delivery.com, datadome.co)", (h, c, u) => AnyHost(u, @"captcha-delivery\.com|datadome\.co")),
("akamai", "_abck / bm_sz cookie", (h, c, u) => c.Any(x => x is "_abck" or "bm_sz" or "ak_bmsc" or "bm_sv")),
("akamai", "akamai header", (h, c, u) => h.Keys.Any(k => k.StartsWith("akamai") || k.StartsWith("x-akamai"))),
("human", "_px cookie", (h, c, u) => c.Any(x => x.StartsWith("_px"))),
("human", "HUMAN host (px-cdn.net, px-cloud.net)", (h, c, u) => AnyHost(u, @"px-cdn\.net|px-cloud\.net|perimeterx\.net|pxchk\.net")),
("kasada", "x-kpsdk header", (h, c, u) => h.Keys.Any(k => k.StartsWith("x-kpsdk"))),
("kasada", "ips.js / kpsdk request", (h, c, u) => u.Any(x => x.Contains("/ips.js") || x.Contains("kpsdk"))),
("imperva", "x-iinfo header", (h, c, u) => h.ContainsKey("x-iinfo")),
("imperva", "incap_ses / visid_incap cookie", (h, c, u) => c.Any(x => Regex.IsMatch(x, "^(incap_ses|visid_incap|nlbi_)"))),
("awswaf", "aws-waf-token cookie", (h, c, u) => c.Contains("aws-waf-token")),
("awswaf", "AWS WAF host (awswaf.com)", (h, c, u) => AnyHost(u, @"awswaf\.com")),
("recaptcha", "reCAPTCHA script", (h, c, u) => u.Any(x => x.Contains("google.com/recaptcha") || x.Contains("recaptcha.net"))),
("hcaptcha", "hCaptcha host", (h, c, u) => AnyHost(u, @"hcaptcha\.com")),
};
var challengeRe = new Regex("just a moment|checking your browser|verify you are human|press & hold|press and hold|are you a robot|human touch", RegexOptions.IgnoreCase);
var blockedRe = new Regex("access denied|access to this page has been denied|attention required|you have been blocked|request blocked|error 1020|pardon our interruption", RegexOptions.IgnoreCase);
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(120) };
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = true });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
// One load in a fresh context: DOM ready + 6 s for the protection scripts to run, then the main
// response's headers, the cookies and every requested URL.
async Task<(string Outcome, List<(string Vendor, string Label)> Hits)> Survey(string url)
{
var context = await browser.NewContextAsync();
var tab = await context.NewPageAsync();
var urls = new List<string>();
tab.Request += (_, r) => { lock (urls) urls.Add(r.Url); };
var h = new Dictionary<string, string>();
string outcome;
try
{
var r = await tab.GotoAsync(url, new() { WaitUntil = WaitUntilState.DOMContentLoaded, Timeout = 45000 });
var status = r?.Status ?? 0;
if (r != null) h = await r.AllHeadersAsync();
await tab.WaitForTimeoutAsync(6000);
var title = await tab.TitleAsync();
string text;
try { text = Regex.Replace(await tab.Locator("body").InnerTextAsync(), @"\s+", " "); } catch (PlaywrightException) { text = ""; }
var frames = string.Join(" ", tab.Frames.Select(f => f.Url));
if (challengeRe.IsMatch($"{title} {text[..Math.Min(800, text.Length)]}") || frames.Contains("captcha-delivery.com")) outcome = "challenge";
else if (blockedRe.IsMatch($"{title} {text[..Math.Min(400, text.Length)]}") || (status is 403 or 429 or 444 && text.Length < 1500)) outcome = "blocked";
else outcome = "content";
}
catch (Exception e) when (e is PlaywrightException or TimeoutException)
{
outcome = "error";
}
var cookies = (await context.CookiesAsync()).Select(c => c.Name).ToList();
await context.CloseAsync();
List<string> snapshot;
lock (urls) snapshot = urls.ToList();
var hits = signals.Where(s => s.Test(h, cookies, snapshot)).Select(s => (s.Vendor, s.Label)).ToList();
return (outcome, hits);
}
try
{
var vendors = vendorOrder.ToDictionary(v => v, v => new JsonObject
{
["vendor"] = v, ["pages"] = 0, ["signals"] = new JsonObject(),
["outcomes"] = new JsonObject { ["content"] = 0, ["challenge"] = 0, ["blocked"] = 0, ["error"] = 0 },
});
var demoPages = new JsonArray();
int none = 0, expectedMatched = 0;
foreach (var page in pages)
{
var (outcome, hits) = await Survey(page.Url);
var found = hits.Select(x => x.Vendor).Distinct().ToList();
if (found.Count == 0) none++;
if (found.Contains(page.Expect)) expectedMatched++;
foreach (var v in found)
{
vendors[v]["pages"] = (int)vendors[v]["pages"]! + 1;
var oc = vendors[v]["outcomes"]!.AsObject();
oc[outcome] = (int)oc[outcome]! + 1;
}
foreach (var (v, label) in hits)
{
var sig = vendors[v]["signals"]!.AsObject();
sig[label] = (sig.ContainsKey(label) ? (int)sig[label]! : 0) + 1;
}
if (page.Demo != null)
demoPages.Add(new JsonObject { ["page"] = page.Demo, ["vendors"] = new JsonArray(found.Select(v => (JsonNode)v!).ToArray()), ["outcome"] = outcome });
}
var output = new JsonObject
{
["measured_at"] = DateTime.UtcNow.ToString("yyyy-MM-dd"),
["pages"] = pages.Length,
["no_vendor_found"] = none,
["expected_vendor_found"] = expectedMatched,
["vendors"] = new JsonArray(vendorOrder.Where(v => (int)vendors[v]["pages"]! > 0).Select(v => (JsonNode)vendors[v]).ToArray()),
["demo_pages"] = demoPages,
};
Console.WriteLine(output.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
await browser.CloseAsync();
}
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"regexp"
"slices"
"strings"
"sync"
"time"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_KEY")
// Public pages, each loaded once: {url, expected vendor, demo name or ""}. Demo pages are the
// protection vendors' own test pages and are reported by name; for the rest only the counts per
// vendor are reported. The expected vendor is what a first survey (2026-10-19) found.
var pages = [][3]string{
{"https://nowsecure.nl/", "turnstile", "nowsecure.nl (Turnstile test page)"},
{"https://www.scrapingcourse.com/cloudflare-challenge", "cloudflare", "scrapingcourse.com Cloudflare challenge"},
{"https://peet.ws/turnstile-test/non-interactive.html", "turnstile", "peet.ws Turnstile test (non-interactive)"},
{"https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php", "recaptcha", "Google reCAPTCHA v3 demo"},
{"https://accounts.hcaptcha.com/demo", "hcaptcha", "hCaptcha demo"},
{"https://www.crunchbase.com/", "cloudflare", ""},
{"https://discord.com/", "cloudflare", ""},
{"https://www.glassdoor.com/", "cloudflare", ""},
{"https://www.tripadvisor.com/", "datadome", ""},
{"https://www.etsy.com/", "datadome", ""},
{"https://soundcloud.com/", "datadome", ""},
{"https://www.homedepot.com/", "akamai", ""},
{"https://www.usps.com/", "akamai", ""},
{"https://www.delta.com/", "akamai", ""},
{"https://www.zillow.com/", "human", ""},
{"https://www.walmart.com/", "human", ""},
{"https://www.realestate.com.au/", "kasada", ""},
{"https://www.ticketmaster.com/", "kasada", ""},
{"https://www.amazon.com/", "awswaf", ""},
{"https://www.booking.com/", "awswaf", ""},
}
var vendorOrder = []string{"cloudflare", "turnstile", "datadome", "akamai", "human", "kasada", "imperva", "awswaf", "recaptcha", "hcaptcha"}
// A lenient host match: url.Parse rejects some of the URLs pages request.
var hostRe = regexp.MustCompile(`^[a-z]+://([^/:?#]+)`)
func host(u string) string {
if m := hostRe.FindStringSubmatch(u); m != nil {
return strings.ToLower(m[1])
}
return ""
}
func anyURL(urls []string, f func(string) bool) bool { return slices.ContainsFunc(urls, f) }
func anyHost(urls []string, re string) bool {
r := regexp.MustCompile(re)
return anyURL(urls, func(u string) bool { return r.MatchString(host(u)) })
}
func anyKey(h map[string]string, prefixes ...string) bool {
for k := range h {
for _, p := range prefixes {
if strings.HasPrefix(k, p) {
return true
}
}
}
return false
}
func has(h map[string]string, k string) bool { _, ok := h[k]; return ok }
func anyCookie(c []string, f func(string) bool) bool { return slices.ContainsFunc(c, f) }
func oneOf(names ...string) func(string) bool {
return func(x string) bool { return slices.Contains(names, x) }
}
type signal struct {
vendor, label string
test func(h map[string]string, c, u []string) bool
}
// Each vendor leaves response headers, cookies or requests to its own hosts.
var signals = []signal{
{"cloudflare", "cf-ray header", func(h map[string]string, c, u []string) bool { return has(h, "cf-ray") }},
{"cloudflare", "__cf_bm / cf_clearance cookie", func(h map[string]string, c, u []string) bool {
return anyCookie(c, oneOf("__cf_bm", "cf_clearance", "__cflb"))
}},
{"cloudflare", "/cdn-cgi/challenge-platform request", func(h map[string]string, c, u []string) bool {
return anyURL(u, func(x string) bool { return strings.Contains(x, "/cdn-cgi/challenge-platform") })
}},
{"turnstile", "Turnstile widget", func(h map[string]string, c, u []string) bool {
return anyURL(u, func(x string) bool {
return strings.Contains(x, "challenges.cloudflare.com") && strings.Contains(x, "turnstile")
})
}},
{"datadome", "x-datadome header", func(h map[string]string, c, u []string) bool { return has(h, "x-datadome") || has(h, "x-dd-b") }},
{"datadome", "datadome cookie", func(h map[string]string, c, u []string) bool { return slices.Contains(c, "datadome") }},
{"datadome", "DataDome host (captcha-delivery.com, datadome.co)", func(h map[string]string, c, u []string) bool {
return anyHost(u, `captcha-delivery\.com|datadome\.co`)
}},
{"akamai", "_abck / bm_sz cookie", func(h map[string]string, c, u []string) bool {
return anyCookie(c, oneOf("_abck", "bm_sz", "ak_bmsc", "bm_sv"))
}},
{"akamai", "akamai header", func(h map[string]string, c, u []string) bool { return anyKey(h, "akamai", "x-akamai") }},
{"human", "_px cookie", func(h map[string]string, c, u []string) bool {
return anyCookie(c, func(x string) bool { return strings.HasPrefix(x, "_px") })
}},
{"human", "HUMAN host (px-cdn.net, px-cloud.net)", func(h map[string]string, c, u []string) bool {
return anyHost(u, `px-cdn\.net|px-cloud\.net|perimeterx\.net|pxchk\.net`)
}},
{"kasada", "x-kpsdk header", func(h map[string]string, c, u []string) bool { return anyKey(h, "x-kpsdk") }},
{"kasada", "ips.js / kpsdk request", func(h map[string]string, c, u []string) bool {
return anyURL(u, func(x string) bool { return strings.Contains(x, "/ips.js") || strings.Contains(x, "kpsdk") })
}},
{"imperva", "x-iinfo header", func(h map[string]string, c, u []string) bool { return has(h, "x-iinfo") }},
{"imperva", "incap_ses / visid_incap cookie", func(h map[string]string, c, u []string) bool {
return anyCookie(c, func(x string) bool {
return strings.HasPrefix(x, "incap_ses") || strings.HasPrefix(x, "visid_incap") || strings.HasPrefix(x, "nlbi_")
})
}},
{"awswaf", "aws-waf-token cookie", func(h map[string]string, c, u []string) bool { return slices.Contains(c, "aws-waf-token") }},
{"awswaf", "AWS WAF host (awswaf.com)", func(h map[string]string, c, u []string) bool { return anyHost(u, `awswaf\.com`) }},
{"recaptcha", "reCAPTCHA script", func(h map[string]string, c, u []string) bool {
return anyURL(u, func(x string) bool {
return strings.Contains(x, "google.com/recaptcha") || strings.Contains(x, "recaptcha.net")
})
}},
{"hcaptcha", "hCaptcha host", func(h map[string]string, c, u []string) bool { return anyHost(u, `hcaptcha\.com`) }},
}
var challengeRe = regexp.MustCompile(`(?i)just a moment|checking your browser|verify you are human|press & hold|press and hold|are you a robot|human touch`)
var blockedRe = regexp.MustCompile(`(?i)access denied|access to this page has been denied|attention required|you have been blocked|request blocked|error 1020|pardon our interruption`)
var spaceRe = regexp.MustCompile(`\s+`)
func launch(name string, options map[string]any) (map[string]any, error) {
body, _ := json.Marshal(options)
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
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
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
}
var session map[string]any
return session, json.NewDecoder(res.Body).Decode(&session)
}
func head(s string, n int) string { return s[:min(n, len(s))] }
// One load in a fresh context: DOM ready + 6 s for the protection scripts to run, then the main
// response's headers, the cookies and every requested URL. Returns the outcome and the signals hit.
func survey(browser playwright.Browser, target string) (string, []signal) {
context, err := browser.NewContext()
if err != nil {
log.Fatal(err)
}
tab, err := context.NewPage()
if err != nil {
log.Fatal(err)
}
var mu sync.Mutex
urls := []string{}
tab.OnRequest(func(r playwright.Request) {
mu.Lock()
defer mu.Unlock()
urls = append(urls, r.URL())
})
h := map[string]string{}
outcome := "error"
if r, err := tab.Goto(target, playwright.PageGotoOptions{WaitUntil: playwright.WaitUntilStateDomcontentloaded, Timeout: playwright.Float(45000)}); err == nil {
status := 0
if r != nil {
status = r.Status()
if all, err := r.AllHeaders(); err == nil {
h = all
}
}
tab.WaitForTimeout(6000)
title, _ := tab.Title()
text, err := tab.Locator("body").InnerText()
if err != nil {
text = ""
}
text = spaceRe.ReplaceAllString(text, " ")
frames := []string{}
for _, f := range tab.Frames() {
frames = append(frames, f.URL())
}
switch {
case challengeRe.MatchString(title+" "+head(text, 800)) || strings.Contains(strings.Join(frames, " "), "captcha-delivery.com"):
outcome = "challenge"
case blockedRe.MatchString(title+" "+head(text, 400)) || (slices.Contains([]int{403, 429, 444}, status) && len(text) < 1500):
outcome = "blocked"
default:
outcome = "content"
}
}
cookies := []string{}
if cs, err := context.Cookies(); err == nil {
for _, c := range cs {
cookies = append(cookies, c.Name)
}
}
context.Close()
mu.Lock()
snapshot := slices.Clone(urls)
mu.Unlock()
hits := []signal{}
for _, s := range signals {
if s.test(h, cookies, snapshot) {
hits = append(hits, s)
}
}
return outcome, hits
}
type outcomes struct {
Content int `json:"content"`
Challenge int `json:"challenge"`
Blocked int `json:"blocked"`
Error int `json:"error"`
}
func (o *outcomes) add(outcome string) {
switch outcome {
case "content":
o.Content++
case "challenge":
o.Challenge++
case "blocked":
o.Blocked++
default:
o.Error++
}
}
type vendor struct {
Vendor string `json:"vendor"`
Pages int `json:"pages"`
Signals map[string]int `json:"signals"`
Outcomes outcomes `json:"outcomes"`
}
type demoPage struct {
Page string `json:"page"`
Vendors []string `json:"vendors"`
Outcome string `json:"outcome"`
}
func main() {
session, err := launch("chromium", map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": true})
if err != nil {
log.Fatal(err)
}
pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
if err != nil {
log.Fatal(err)
}
defer pw.Stop()
browser, err := pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
log.Fatal(err)
}
defer browser.Close()
vendors := map[string]*vendor{}
for _, v := range vendorOrder {
vendors[v] = &vendor{Vendor: v, Signals: map[string]int{}}
}
demoPages := []demoPage{}
none, expectedMatched := 0, 0
for _, page := range pages {
outcome, hits := survey(browser, page[0])
found := []string{}
for _, s := range hits {
if !slices.Contains(found, s.vendor) {
found = append(found, s.vendor)
}
vendors[s.vendor].Signals[s.label]++
}
if len(found) == 0 {
none++
}
if slices.Contains(found, page[1]) {
expectedMatched++
}
for _, v := range found {
vendors[v].Pages++
vendors[v].Outcomes.add(outcome)
}
if page[2] != "" {
demoPages = append(demoPages, demoPage{page[2], found, outcome})
}
}
list := []*vendor{}
for _, v := range vendorOrder {
if vendors[v].Pages > 0 {
list = append(list, vendors[v])
}
}
out, _ := json.MarshalIndent(map[string]any{
"measured_at": time.Now().UTC().Format("2006-01-02"),
"pages": len(pages),
"no_vendor_found": none,
"expected_vendor_found": expectedMatched,
"vendors": list,
"demo_pages": demoPages,
}, "", " ")
fmt.Println(string(out))
}
What we got
| Vendor | Pages (of 20) | Signals seen (pages) | Outcomes | Measured |
|---|---|---|---|---|
| Cloudflare | 7 | cf-ray header 6, __cf_bm / cf_clearance cookie 5, /cdn-cgi/challenge-platform request 6 | content 5, challenge 2 | 2026-10-20 |
| Cloudflare Turnstile | 4 | Turnstile widget 4 | content 2, challenge 2 | 2026-10-20 |
| DataDome | 3 | x-datadome header 2, datadome cookie 3, DataDome host (captcha-delivery.com, datadome.co) 2 | content 1, challenge 2 | 2026-10-20 |
| Akamai | 3 | akamai header 3, _abck / bm_sz cookie 1 | content 1, blocked 2 | 2026-10-20 |
| HUMAN (PerimeterX) | 2 | HUMAN host (px-cdn.net, px-cloud.net) 2, _px cookie 1 | content 1, challenge 1 | 2026-10-20 |
| Kasada | 2 | x-kpsdk header 1, ips.js / kpsdk request 2 | blocked 2 | 2026-10-20 |
| AWS WAF | 2 | aws-waf-token cookie 2, AWS WAF host (awswaf.com) 1 | content 2 | 2026-10-20 |
| Google reCAPTCHA | 1 | reCAPTCHA script 1 | content 1 | 2026-10-20 |
| hCaptcha | 1 | hCaptcha host 1 | content 1 | 2026-10-20 |
From the Node.js run on 2026-10-20. 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-10-20",
"pages": 20,
"no_vendor_found": 0,
"expected_vendor_found": 20,
"vendors": [
{
"vendor": "cloudflare",
"pages": 7,
"signals": {
"cf-ray header": 6,
"__cf_bm / cf_clearance cookie": 5,
"/cdn-cgi/challenge-platform request": 6
},
"outcomes": {
"content": 5,
"challenge": 2,
"blocked": 0,
"error": 0
}
},
{
"vendor": "turnstile",
"pages": 4,
"signals": {
"Turnstile widget": 4
},
"outcomes": {
"content": 2,
"challenge": 2,
"blocked": 0,
"error": 0
}
},
{
"vendor": "datadome",
"pages": 3,
"signals": {
"x-datadome header": 2,
"datadome cookie": 3,
"DataDome host (captcha-delivery.com, datadome.co)": 2
},
"outcomes": {
"content": 1,
"challenge": 2,
"blocked": 0,
"error": 0
}
},
{
"vendor": "akamai",
"pages": 3,
"signals": {
"akamai header": 3,
"_abck / bm_sz cookie": 1
},
"outcomes": {
"content": 1,
"challenge": 0,
"blocked": 2,
"error": 0
}
},
{
"vendor": "human",
"pages": 2,
"signals": {
"HUMAN host (px-cdn.net, px-cloud.net)": 2,
"_px cookie": 1
},
"outcomes": {
"content": 1,
"challenge": 1,
"blocked": 0,
"error": 0
}
},
{
"vendor": "kasada",
"pages": 2,
"signals": {
"x-kpsdk header": 1,
"ips.js / kpsdk request": 2
},
"outcomes": {
"content": 0,
"challenge": 0,
"blocked": 2,
"error": 0
}
},
{
"vendor": "awswaf",
"pages": 2,
"signals": {
"aws-waf-token cookie": 2,
"AWS WAF host (awswaf.com)": 1
},
"outcomes": {
"content": 2,
"challenge": 0,
"blocked": 0,
"error": 0
}
},
{
"vendor": "recaptcha",
"pages": 1,
"signals": {
"reCAPTCHA script": 1
},
"outcomes": {
"content": 1,
"challenge": 0,
"blocked": 0,
"error": 0
}
},
{
"vendor": "hcaptcha",
"pages": 1,
"signals": {
"hCaptcha host": 1
},
"outcomes": {
"content": 1,
"challenge": 0,
"blocked": 0,
"error": 0
}
}
],
"demo_pages": [
{
"page": "nowsecure.nl (Turnstile test page)",
"vendors": [
"cloudflare",
"turnstile"
],
"outcome": "content"
},
{
"page": "scrapingcourse.com Cloudflare challenge",
"vendors": [
"cloudflare",
"turnstile"
],
"outcome": "challenge"
},
{
"page": "peet.ws Turnstile test (non-interactive)",
"vendors": [
"cloudflare",
"turnstile"
],
"outcome": "content"
},
{
"page": "Google reCAPTCHA v3 demo",
"vendors": [
"recaptcha"
],
"outcome": "content"
},
{
"page": "hCaptcha demo",
"vendors": [
"cloudflare",
"hcaptcha"
],
"outcome": "content"
}
]
}Takeaways
- One page load is enough to name the vendor. On 2026-10-20 the detector found the vendor seen the day before on 19–20 of the 20 pages in each of the five runs, and at most one page per run showed no signal. Imperva didn't show up on any of these pages.
- Most pages carry more than one vendor. All the Turnstile, Cloudflare-challenge and hCaptcha demo pages are themselves served through Cloudflare, so Cloudflare was found on 7–9 pages while the
cf-rayheader was on 6 in every run. Count the vendor that decides the outcome, not every vendor present. - A header shows the CDN; a cookie or script shows the bot product. The
akamai-*header was on all 3 Akamai pages in every run, but the Bot Manager cookie (_abck/bm_sz) on only 1–3 of them. The same goes forcf-ray(Cloudflare's CDN) versus__cf_bmand/cdn-cgi/challenge-platform(its bot management). The cookie set by scripts after load is the stronger signal, which is why the detector waits 6 s. - What the baseline saw (headless Chromium through your own proxy, 2026-10-20): DataDome showed its CAPTCHA (a
captcha-delivery.comframe) on 2 of its 3 pages in every run; Kasada returned a block on every page where it was detected (1–2); Akamai blocked 1–2 of 3; HUMAN showed a challenge on 1 of its 2–3; both AWS WAF pages loaded as content. These are dated snapshots of one setup on one day; the cases later this week compare browsers and networks on the same vendors. - The vendors' own demo pages were stable across all five runs: nowsecure.nl, the peet.ws Turnstile test, Google's reCAPTCHA v3 demo and the hCaptcha demo loaded as
contentwith their widget present, and scrapingcourse.com's Cloudflare challenge stayed on its interactive challenge. Nothing was solved or retried; a widget being present is not a pass.