- A passed challenge proves a token was bought for your sitekey; 2Captcha lists Turnstile solves at €1.40 per thousand.
- Five families survive stealth patches: automation surfaces, environment coherence, TLS vs user agent, network origin, device history.
- The check sits on the server between the form POST and the side effect; the browser never sees the verdict.
- Block on automation_detected, antidetect_browser, tor_exit_node and proxy_detected; hold, never block, on a VPN or datacenter alone.
- Log decision, reasons[], network.service and device.times_seen for a full cycle before enforcing; no rule reads tampering_score.
On this page
A passed challenge proves one thing: somebody obtained a token for your sitekey within the last few minutes. On 2Captcha's price list (checked 2026-08-29) that token costs €1.40 per thousand for Cloudflare Turnstile and for a reCAPTCHA v3 score of 0.3 or under, and the request carrying it arrives from a real Chrome build on a home IP. The challenge never saw the automation; it saw a browser that could wait for a solver. This playbook covers the bot run as the operator sets it up, the five signal families that survive stealth patches, where the verdict goes, and a ladder that shows nobody a puzzle.
How a bot run is set up in 2026
Like every entry in the fraud playbooks, this one starts with the operator's setup. Four parts, each a product with a price list.
- A browser runtime. Puppeteer or Playwright driving Chrome's new headless mode, "the real Chrome browser" rather than the old stripped shell (Chrome for Developers, checked 2026-08-29). On top, puppeteer-extra-plugin-stealth, which patches
navigator.webdriver,chrome.runtime,webgl.vendorand the rest of its evasion list, or rebrowser-patches, which works on the DevTools protocol itself. For account farming, an antidetect browser (Kameleo, Multilogin, GoLogin, AdsPower) swaps profiles instead. - An exit per identity. A residential pool sold by the gigabyte (Bright Data, Oxylabs, IPRoyal, Decodo), one home line per session. How residential proxies are sourced and detected covers the supply side.
- A solver. The bot posts your challenge to 2Captcha or Anti-Captcha and waits for the token. Anti-Captcha quotes Turnstile at $2 per thousand and reCAPTCHA Enterprise at $5 (checked 2026-08-29).
- A script, or a model. A hand-written flow for one target, or an LLM agent that reads the page and fills the form. They look different in the DOM and identical in your access log.
The run enters your stack as the same POST your own form sends: signup, login, coupon redemption, add-to-cart. None of it is a page view, so a rule written for crawlers never fires on it.
LLM-driven bots and agent runtimes
The scripted bot needed a developer per target; an agent runtime removes that cost. Browser Use drives Playwright from a model prompt, showed 111.7k GitHub stars when we checked on 2026-08-29, and its hosted tier advertises "best stealth with proxy rotation and captcha solving." The same runtime carries a shopper who asked an assistant to buy a ticket and a crew opening two hundred accounts, so the automation signal alone no longer says which.
Operators who want to be recognized can be. OpenAI publishes IP ranges for GPTBot, OAI-SearchBot and ChatGPT-User, and notes that for ChatGPT-User "robots.txt rules may not apply" because the fetch is user-initiated (OpenAI bot docs, checked 2026-08-29). The IETF draft HTTP Message Signatures for automated traffic (draft-02, dated 2026-08-18) defines a Signature-Agent header that verifies against a published key; a request that verifies is a population you can name and give its own policy. Verifying declared agents is covered in telling AI assistants from attackers, the policy question in whether to block AI agents at all. A user-agent string that merely says "agent" goes through the five families like everything else.
Why a CAPTCHA misses it
A challenge tests whether the client can produce a token, and every product now sells a mode that produces one without a puzzle: reCAPTCHA v3 "will never interrupt your users" and returns a score from 0.0 to 1.0 with 0.5 as the default threshold (Google docs, checked 2026-08-29); Turnstile's non-interactive and invisible widgets run "proof-of-work (computational puzzles), proof-of-space, probing for web APIs" (Cloudflare docs); hCaptcha's Pro plan lists a "99.9% Passive Mode" (hCaptcha pricing). Two things follow.
- The token is the product. A Turnstile token "is valid for 300 seconds (5 minutes) after generation" and "can only be validated once" (siteverify docs, checked 2026-08-29). That stops replay, not a solver returning a fresh token per request; the widget sees the tab the bot controls, not the machine.
- The score is priced. 2Captcha charges €1.40 per thousand for a v3 score of 0.3 or lower and €2.80 above it. The bypass market quotes your threshold back at you.
Why solver prices below a cent broke the CAPTCHA model works through the economics; a passed challenge is evidence of a budget, not of a person. We make the last row of the table.
| Control | Friction | Solver price per 1,000 (checked 2026-08-29) | What it sees |
|---|---|---|---|
| reCAPTCHA v3 | None; a badge | €1.40 at score ≤ 0.3, €2.80 above | Page interactions; it "learns by seeing real traffic on your site" |
| hCaptcha | Puzzle on Basic; passive on Pro and Enterprise | Not on either list we checked | Its own challenge in the widget |
| Turnstile | Managed shows a checkbox on risk; non-interactive and invisible show nothing | €1.40 (2Captcha), $2 (Anti-Captcha) | Browser quirks and proof-of-work inside the bot's tab; a cross-session ephemeral_id only on Enterprise, expiring "within a few days" (Cloudflare docs) |
Server-side verdict (Maskbreak /v1/evaluate) |
None; nothing rendered | No token to buy; the bot needs a new machine and a new exit | Automation surfaces, environment coherence, network origin, device history across sessions |
Run your own signup or checkout traffic through this: the free scanner returns the same verdict the API does.
Open the scannerThe tells: five signal families and what defeats each
Grouped by what the operator has to replace to beat them; the sections after the table say where each family breaks.
| Tell | Verdict field / reason code | Weight |
|---|---|---|
navigator.webdriver true, Runtime.Enable artifacts, sourceURL=pptr: stamps | device.automation / automation_detected | block |
| User agent claims an engine the runtime is not | device.antidetect / antidetect_browser | block |
| WebGL renderer the hardware cannot produce | device.antidetect / antidetect_browser | block |
| Tor exit | network.tor / tor_exit_node | block |
| Residential or SOCKS proxy, provider attributed | network.proxy (legacy details.proxied), network.service / proxy_detected | block |
| Datacenter ASN on a consumer flow | network.datacenter (legacy details.dch) / datacenter_asn | step-up |
| Commercial VPN | network.vpn, network.service / vpn_detected | step-up |
| Browser clock in a country the exit is not, on an ordinary connection | timezone.matches_ip false with timezone.checked true / timezone_mismatch | step-up |
| One device across many accounts | device.linked_accounts, device.multi_account / multi_account_device | step-up |
| Device never seen before, on a high-value action | device.times_seen 1, device.first_seen | evidence |
| Device evaluated unusually often | device.high_activity / high_activity_device | evidence |
| Virtual machine | device.virtual_machine / virtual_machine | evidence |
| TLS handshake (JA4) contradicts the user agent | Not in the verdict; read at the edge | evidence |
1. Automation surfaces: WebDriver and the DevTools protocol
The WebDriver specification defines navigator.webdriver as "a standard way for co-operating user agents to inform the document that it is controlled by WebDriver" (W3C, checked 2026-08-29). The stealth plugin removes that flag first, but not the protocol it runs on. The rebrowser-patches README says "popular automation libraries rely on the CDP command Runtime.Enable," that Puppeteer "adds //# sourceURL=pptr:... to every script in page.evaluate()," and that patched console and navigator objects can be detected too: "it's always a cat-and-mouse game."
What defeats it: a patched driver plus the discipline of touching nothing on the JavaScript surface, which holds for one target and gets expensive across many; the stealth maintainers themselves aim to make detection "cost-prohibitive or triggers too many false-positives," not impossible. In the verdict this family is device.automation with reason automation_detected, and it needs the device token: send fingerprintEventId or you get a network-only verdict. Which headless probes still fire on Chrome's new headless mode and the framework-by-probe view for Puppeteer and Playwright go deeper.
2. Environment coherence: does the story hold together?
On 2026-08-12 we ran spoofed profiles through the homepage scanner's device path (/api/device-intel; the camelCase fields are scanner output, mapping to device.antidetect in the API). An unspoofed WKWebView came back browserTampering: false. A profile claiming Chrome 131 on Windows while running WebKit came back true; a user agent changed to a Safari string consistent with the engine passed. A WebGL hook returning the machine's real renderer passed; one returning "Apple M1 Max" fired. Every other single change, from screen size to canvas noise, passed alone.
Two lessons. It is a consistency check, not a change detector: lying is fine as long as every layer tells the same lie, and the tell is the layer the tool forgot. And the score is not the signal: tamperingScore read 0.0029 on every profile, spoofed or not, so we build no rule on device.tampering_score and route on the boolean and the reason code antidetect_browser. What defeats it: a profile whose every surface agrees with the real engine, which is what commercial antidetect browsers sell; a second machine is not for sale, which is family five. How antidetect browsers are detected, tool by tool and the Kameleo and Undetectable live run show it on shipping products.
3. TLS and HTTP/2 versus the user agent
A Python requests client with a Chrome user agent still opens TLS like Python. JA3 and JA4 "identify TLS clients based on how they initiate connections" and are "a stable identifier across different destination IPs, ports, and certificates" (Cloudflare docs, checked 2026-08-29). A user agent claiming Chrome on a Go or Python ClientHello is a contradiction no JavaScript patch reaches; the same page notes that similar clients share a fingerprint, so it identifies the stack, not the person.
What defeats it: a real browser; Playwright driving real Chrome presents Chrome's TLS. This family does not live in our verdict, since /v1/evaluate returns no TLS field, so read it at your edge and log it beside the verdict; see what a JA4 string decodes to and where to read it and what a Cloudflare Worker can and cannot see.
4. Network origin
Datacenter ranges, Tor exits and attributed proxy and VPN services are the oldest family and still the one the cheapest bots fail. The verdict carries network.datacenter, network.tor, network.proxy, network.vpn and network.service, the last naming the provider when known. The engine blocks on an attributed proxy and on Tor by itself; a VPN alone moves the decision to review.
What defeats it: a residential exit, which is why the operator buys one. Read network.residential: true as the absence of a datacenter or proxy hit, not a clearance. The timezone block adds a cheap contradiction: when the browser's clock belongs to a different country than an ordinary-looking exit, timezone.matches_ip is false, timezone_mismatch fires and allow becomes review. On a VPN, proxy, Tor or datacenter exit timezone.checked is false and the mismatch raises nothing; it never blocks on its own, because travelers land there too.
5. Cross-session device velocity
Everything above scores one request. A bot is a pattern across requests, and the pattern lives on the device, not on the IP the operator swaps per session. device.visitor_id is stable across sessions and cookie clears; device.times_seen and device.first_seen give the history under your key; with your own accountId in the request, device.linked_accounts and device.multi_account count the accounts this device has touched; device.high_activity flags a device evaluated unusually often. All of it is stored as one-way hashes under your key.
What defeats it: a new machine per identity, and the lab says so, with one caveat. In the same 2026-08-12 run a profile switch on one laptop did mint a distinct, stable id per profile early on, and linked_accounts did not join them; the tampering flag is what caught those. Once the matcher had about thirty sightings from the machine, nothing moved the id: every JavaScript-reachable value was changed, canvas and WebGL readback were perturbed inside a rebuilt WebKit, and it still came back as the same device. The run's limits: one machine, one IP, nothing below JavaScript except the two readback patches. Linking one person to hundreds of accounts is the multi-accounting side of this family.
Where the check goes
After the POST reaches your server and before the side effect: the account row, the session cookie, the payment intent. Not on page views, where nothing is at stake. Not in the browser, where the bot can read the answer.
The client half is one script and one class on the form. The SDK fills two hidden inputs, monocle for the network token and sentinel_fp for the device event id, and Sentinel.collect() returns both once the device layer has settled (sentinel is the SDK's object name, kept from the product's original name so integrations did not break).
<script async src="https://maskbreak.com/assets/sentinel.js"></script>
<form class="monocle-enriched" id="signup">
<input type="email" name="email" required>
<button>Create account</button>
</form>
<script>
document.getElementById('signup').addEventListener('submit', async (e) => {
e.preventDefault();
const { token, fingerprintEventId } = await window.Sentinel.collect();
await fetch('/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: e.target.email.value, token, fingerprintEventId })
});
});
</script>
The server half is the decision point, with two rules built in. The call has a timeout and fails open with a counter, because an outage that blocks every signup is a worse incident than an hour of unscored traffic. The block response has the shape and status of a validation failure, so the operator cannot tell a verdict from a typo. The documented sample responses show evaluated_in_ms of 118 and 126; we give the client 1,500 ms and fall through past it.
// npm install @sentinelsup/sdk (SENTINEL_KEY=sk_test_... until ENFORCE goes on)
const Sentinel = require('@sentinelsup/sdk');
const sentinel = new Sentinel({ apiKey: process.env.SENTINEL_KEY, timeoutMs: 1500 });
app.post('/signup', async (req, res) => {
const { email, token, fingerprintEventId } = req.body;
let verdict;
try {
verdict = await sentinel.evaluate({ token, fingerprintEventId, email });
} catch (err) {
metrics.increment('evaluate.unavailable'); // alert on the rate, not the event
verdict = { decision: 'allow', risk_score: null, reasons: [], fallback: true };
}
log.info({
decision: verdict.decision, risk_score: verdict.risk_score, reasons: verdict.reasons,
service: verdict.network && verdict.network.service,
times_seen: verdict.device && verdict.device.times_seen,
ms: verdict.evaluated_in_ms, fallback: verdict.fallback === true
}, 'signup.evaluate');
if (ENFORCE && verdict.decision === 'block') {
// same shape and status as a validation error; never echo reasons[]
return res.status(400).json({ error: 'We could not create your account.' });
}
const user = await users.create({ email });
if (verdict.decision === 'review') await holds.queue(user.id, verdict.reasons);
return res.status(201).json({ ok: true });
});
Observe-only first: what to log
Ship with ENFORCE off and log for at least one full cycle of your traffic. The log line above is the minimum; add device.linked_accounts once you pass accountId, and join each line to outcomes: refunds, chargebacks, later bans. Read the three bands against those outcomes before enforcing, hard reasons first. The sandbox key exercises the routing without an account; it answers only the test tokens and the response carries "test": true.
curl -s -X POST https://maskbreak.com/v1/evaluate \
-H "Authorization: Bearer sk_test_sandbox" \
-H "Content-Type: application/json" \
-d '{"token":"test_proxy"}' # decision: block -> your 400 path
# test_vpn -> decision: review -> account created, hold queued
# test_clean -> decision: allow -> 201
# Your own sk_test_ key (Settings -> API Key) runs the full pipeline with real tokens,
# badged as test in the console and excluded from usage.
Response ladder
decision is advisory: our reading of the signals, which your dashboard rules can override while risk_score stays what the engine computed. Route on the decision, sort your queue on the score is the rule we use ourselves. In every tier the user sees nothing extra.
| Tier | Evidence it needs | Response |
|---|---|---|
| Allow | reasons[] empty, or only virtual_machine or high_activity_device on a low-value action |
Proceed. Keep the log line; the history is what scores the next visit. |
| Review | One of vpn_detected, datacenter_asn, timezone_mismatch, multi_account_device, disposable_email; or times_seen 1 on a payout, coupon or referral |
Proceed with a silent hold: verify the email before the reward, delay the payout, queue anything with money attached for a person. |
| Block | automation_detected, antidetect_browser, emulator_detected, tor_exit_node, proxy_detected, or a rule of your own |
Refuse with the shape and timing of a validation failure. Never name the reason. Log reasons[] and device.visitor_id. |
The block tier is the engine's own hard set, the one the bot detection API page documents: proxy, Tor, automation, emulator and antidetect come back as block from the pipeline itself; a VPN alone comes back as review. The ladder never blocks on vpn_detected or datacenter_asn alone (a VPN on a login is a commuter), and it never reads device.tampering_score. The signup hold flow is drawn out on stopping fake signups.
What it costs you
Two bills: the control and the friction. The control prices are published. reCAPTCHA's Essentials tier is "free up to 10,000 assessments per calendar month"; Premium charges an "$8.00 flat fee" for 10,001–100,000 assessments a month and "$1.00 per 1,000 assessments" above that; Enterprise is "$1 per 1,000 assessments" on a twelve-month minimum (Google Cloud tiers, checked 2026-08-29). hCaptcha's Basic plan is free; Pro is $99 a month billed yearly or $139 monthly, with "100K monthly evals included, then $0.99/1K"; risk scores are Enterprise (hCaptcha pricing). Turnstile is free for up to 20 widgets with 10 hostnames each; Ephemeral IDs and unlimited widgets are Enterprise (Cloudflare plans). Maskbreak's free tier is 1,000 requests an hour per key (API reference).
The friction bill has no published number we would repeat. Measure it in your funnel: challenge shown, challenge passed, form submitted, before and after. The server-side verdict renders nothing, so its friction line is zero by construction; its cost is the review queue.
Checklist
- The check runs on the POST that creates the side effect, never on page views and never from the browser.
fingerprintEventIdis forwarded with the token, or the automation and antidetect checks never run;accountIdis sent sodevice.linked_accountsexists.- The client timeout is explicit, the failure path returns allow, and the failure rate has an alert.
- The block response is byte-for-byte a validation error;
reasons[]goes to the log, never to the client. - Enforcement started with the hard reasons after an observe-only cycle; the review band is a hold, not a refusal.
- No rule reads
device.tampering_score; no rule blocks onvpn_detectedordatacenter_asnalone. - JA4 is logged at the edge beside the verdict; declared agents are verified by published ranges or a
Signature-Agentsignature, not a user-agent string.
Questions people ask
- How do you detect bots without a CAPTCHA?
- Score the request on the server, after the form POST and before the side effect. Read automation surfaces (device.automation), environment coherence (device.antidetect), network origin (network.proxy, network.tor, network.datacenter, network.vpn) and device history (device.times_seen, device.linked_accounts) in one verdict, log reasons[] for a full cycle, then enforce the hard reasons first. The visitor sees nothing; a block is shaped like a validation error.
- Can bots bypass reCAPTCHA v3 and Turnstile?
- Yes, at a published price. On 2026-08-29 2Captcha listed Turnstile and a reCAPTCHA v3 score of 0.3 or under at €1.40 per thousand, and Anti-Captcha listed Turnstile at $2. A solved token proves that someone bought a token for your sitekey in a real browser the widget could not see past. That is why the check has to read the machine and its history, not the tab.
- Does Maskbreak detect Puppeteer and Playwright with stealth plugins?
- Automation comes back as device.automation with the automation_detected reason code, and the bot detection page documents that patched Puppeteer, Playwright and Selenium sessions still return it. No detector stays ahead of every framework forever, which is why the verdict also carries network origin and device history, and why we publish no detection rate. The check needs fingerprintEventId; without it you get a network-only verdict.
- How do I detect AI agents like Browser Use on my site?
- An undeclared agent is automation on a residential exit and goes through the same five signal families as a scripted bot. A declared one can be verified: OpenAI publishes IP ranges for GPTBot, OAI-SearchBot and ChatGPT-User, and the IETF Web Bot Auth draft defines a Signature-Agent header you can check against a published key. Verify, then give that population its own per-action policy.
Put the check where the attack enters
One call before signup, login or checkout returns decision, risk_score and the reasons behind them. The free tier is 1,000 requests an hour, no card required. Start with <a href="/vpn-detection">VPN detection</a> and <a href="/proxy-detection">proxy detection</a>, the network layer most attacks lean on.