CAPTCHA solving
Cloudflare Turnstile, reCAPTCHA v3, hCaptcha — solved automatically.
Setup
The default solver is CapSolver. Set your key and you're done:
CAPSOLVER_API_KEY=cap_xxxxxxxxxxxx
CAPSOLVER_TIMEOUT_S=120Cloudflare Turnstile
The browser tier loads the page, the block detector flags Turnstile, the sitekey is extracted from the DOM, the solver returns a token in ~5s, and we inject it into the hidden response field before resubmitting.
reCAPTCHA v3
v3 is score-based — there's no puzzle to solve. The solver simulates a clean session and returns a token with a target score. We inject and continue.
hCaptcha
Visual challenges are solved by the solver's vision model (CapSolver's HCaptchaTurboTask). Token returned, injected, page resubmitted.
Bring your own solver
The CaptchaSolver Protocol is two methods. Implement it for any provider, pass the instance into the Orchestrator, and it slots into the same tier flow.
Per-target solver hint
Auto-detection looks for cf-turnstile, g-recaptcha, and hcaptcha.com markers in the rendered HTML. That misses widgets rendered in shadow DOM, lazy-loaded after our content snapshot, or wrapped in a custom React component. When you already know what a target ships, set captcha_hint on the job:
{
"name": "fintech-signup-leads",
"urls": ["https://example-bank.com/open-account"],
"max_tier": 2,
"captcha_hint": "hcaptcha"
}Accepted values: turnstile · recaptcha_v3 · hcaptcha. With a hint set, Tier 2 still extracts the sitekey from the page; the hint only tells the solver which task type to submit.
Cost transparency
Every successful Tier-2 fetch records solver_cost_usd on the result row, sourced from CapSolver's price field where present. Per-task fallbacks (when the API doesn't echo the price): Turnstile ~$0.0008, reCAPTCHA v3 ~$0.0010, hCaptcha ~$0.0008. The scrape_solver_cost_usd_total Prometheus counter tracks lifetime spend; the dashboard /jobs/:id view shows it per-fetch.
Test sitekey safety
Cloudflare publishes test sitekeys (1x00…AA always-pass, 2x00…AB always-block, 3x00…FF always-interactive). Real solvers reject these with HTTP 400 because they don't correspond to a customer site. Scrape detects them locally and skips with captcha.skipped_test_sitekey so we don't waste a roundtrip on a never-going-to-solve task.