Built-in CAPTCHA detection with configurable handling strategies for browser automation agents.
Predicate is designed as infrastructure for token-efficient browser automation runtime, not as a full-service automation provider. We deliberately limit our scope to detection and verification for two reasons:
As an infrastructure layer, Predicate focuses on providing reliable, efficient browser automation primitives. CAPTCHA resolution involves domain-specific policies, third-party integrations, and compliance considerations that vary significantly across organizations and use cases.
Different organizations have distinct security policies, legal requirements, and acceptable use guidelines governing CAPTCHA handling. By delegating resolution to customer-controlled systems, Predicate avoids imposing a one-size-fits-all approach and enables customers to implement solutions that align with their specific compliance obligations.
Predicate detects CAPTCHAs, pauses execution, and verifies clearance. You decide how to resolve them using your own workflows or external systems that comply with your organization's security policies.
When the Predicate SDK detects a CAPTCHA during browser automation, the following flow occurs:
snapshot.diagnostics.captcha with a confidence score.captcha.detected == true and confidence >= minConfidence, the runtime invokes your configured policy.wait_until_cleared, the runtime continuously re-snapshots until captcha.detected == false or timeout is reached.CAPTCHAs vary by site and session. A practical approach is a hybrid fallback chain that starts with token-based solvers (best coverage for standard providers), then tries OCR for image-only challenges, and finally uses a vision LLM as a last resort.
This design keeps the SDK responsibilities focused on detection + orchestration, while you control the actual solving logic and providers (e.g., a token service like 2Captcha, or an internal OCR pipeline).
High-level behavior:
<img> and nearby input.Signal source: CAPTCHA detection is surfaced on the current page via snapshot.diagnostics.captcha. This signal is only reliable on the page where the CAPTCHA is actually rendered (e.g., after a modal opens or a challenge iframe appears).
Pseudocode (Python):
from predicate.captcha import CaptchaContext
def solve_captcha(ctx: CaptchaContext):
if ctx.captcha.provider_hint in {"recaptcha", "hcaptcha", "turnstile"}:
if try_token_solver(ctx): # e.g., external provider
return wait_until_cleared(ctx)
if has_image_captcha(ctx): # visible <img> + nearby input
if try_ocr_solver(ctx): # image → text
return wait_until_cleared(ctx)
# Best-effort fallback
if try_vision_solver(ctx): # vision LLM on captured image
return wait_until_cleared(ctx)
return abort("captcha_unresolved")For questions or guidance, contact
Predicate Labs Support
When you run a CAPTCHA handler, the runtime provides a page-control hook so your solver can read or write small page state inside the same live browser session. This is what makes token injection or callback triggering possible without reloading the page.
Use it sparingly and keep the JS payload small and bounded.
from predicate.captcha import CaptchaContext
async def external_solver(ctx: CaptchaContext):
# Minimal, bounded JS: read a site key and inject a token.
sitekey = await ctx.page_control.evaluate_js("/* read sitekey from DOM */")
token = await request_token(sitekey) # your external system
await ctx.page_control.evaluate_js("/* write token into response field */")
return {"action": "wait_until_cleared"}Policies determine what happens when a CAPTCHA is detected:
| Policy | Behavior |
|---|---|
abort | Stop execution immediately when CAPTCHA is detected. Safest default for workflows where CAPTCHA indicates an unexpected state. |
callback | Invoke your custom handler function once per CAPTCHA incident, allowing you to decide the appropriate action dynamically. |
Actions are the runtime behaviors your handler can request:
| Action | Behavior | Details |
|---|---|---|
abort | Terminate the run | Sets reason_code to captcha_policy_abort |
retry_new_session | Reset and retry | Closes current browser session, opens fresh one, retries from beginning. Bounded by maxRetriesNewSession (default: 3) |
wait_until_cleared | Pause and poll | Suspends execution, periodically re-snapshots until CAPTCHA is no longer detected or timeoutMs expires |
| Option | Type | Default | Description |
|---|---|---|---|
minConfidence | number | 0.7 | Minimum confidence threshold (0-1) to trigger CAPTCHA handling |
timeoutMs | number | 120000 | Maximum time (ms) to wait for CAPTCHA clearance |
pollMs | number | 1000 | Interval (ms) between re-snapshot attempts during wait_until_cleared |
maxRetriesNewSession | number | 3 | Maximum retry_new_session attempts before aborting |
Predicate provides three built-in strategy helpers. These are not solvers; they configure the runtime's response to CAPTCHA detection and rely on external systems or humans for actual resolution.
| Strategy | Purpose | Use Case |
|---|---|---|
HumanHandoffSolver | Pause execution and signal a human operator | Live sessions, monitoring dashboards, manual intervention workflows |
VisionSolver | Use vision to confirm clearance (no clicking/typing) | Automated verification after external resolution |
ExternalSolver | Call your webhook/service, then wait for clearance | Integration with third-party CAPTCHA services or custom internal systems |
Immediately stop when CAPTCHA is detected. No resolution attempted.
from predicate import AgentRuntime, CaptchaOptions
runtime.set_captcha_options(
CaptchaOptions(
policy="abort",
min_confidence=0.7,
)
)When CAPTCHA is detected, the runtime pauses and waits for a human to solve it in the live browser session.
from predicate import CaptchaOptions, HumanHandoffSolver
runtime.set_captcha_options(
CaptchaOptions(
policy="callback",
handler=HumanHandoffSolver(),
min_confidence=0.7,
timeout_ms=120_000, # Wait up to 2 minutes
poll_ms=1_000, # Re-check every second
)
)Uses vision to confirm the CAPTCHA has cleared. Does not click or type. Useful after an external system has already solved the CAPTCHA.
from predicate import CaptchaOptions, VisionSolver
runtime.set_captcha_options(
CaptchaOptions(policy="callback", handler=VisionSolver())
)Calls your webhook/service when CAPTCHA is detected, then waits for clearance. Your external system performs the actual resolution.
from predicate import CaptchaOptions, ExternalSolver
async def notify_webhook(ctx) -> None:
"""
Example hook: send context to your external system.
Replace with your own client / queue / webhook call.
Predicate does NOT implement solver logic.
"""
print(f"[captcha] external resolver notified: url={ctx.url} run_id={ctx.run_id}")
# Example: await your_http_client.post(webhook_url, json={...})
runtime.set_captcha_options(
CaptchaOptions(
policy="callback",
handler=ExternalSolver(lambda ctx: notify_webhook(ctx)),
timeout_ms=180_000, # 3 minutes for external service
)
)import asyncio
import os
from predicate import (
AgentRuntime,
AsyncPredicateBrowser,
CaptchaOptions,
ExternalSolver,
HumanHandoffSolver,
VisionSolver,
)
from predicate.tracing import JsonlTraceSink, Tracer
async def notify_webhook(ctx) -> None:
"""
Example hook: send context to your external system.
Replace with your own client / queue / webhook call.
Predicate does NOT implement solver logic.
"""
print(f"[captcha] external resolver notified: url={ctx.url} run_id={ctx.run_id}")
# Example: await your_http_client.post(webhook_url, json={...})
async def main() -> None:
tracer = Tracer(run_id="captcha-demo", sink=JsonlTraceSink("trace.jsonl"))
async with AsyncPredicateBrowser() as browser:
page = await browser.new_page()
runtime = await AgentRuntime.from_sentience_browser(
browser=browser,
page=page,
tracer=tracer,
)
# ---------------------------------------------------------------------
# Option 1: Human-in-loop (recommended for live sessions)
# ---------------------------------------------------------------------
# When CAPTCHA is detected, the runtime pauses and waits for a human
# to solve it in the live browser session.
runtime.set_captcha_options(
CaptchaOptions(
policy="callback",
handler=HumanHandoffSolver(),
min_confidence=0.7,
timeout_ms=120_000, # Wait up to 2 minutes
poll_ms=1_000, # Re-check every second
)
)
# ---------------------------------------------------------------------
# Option 2: Vision-only verification (no DOM actions)
# ---------------------------------------------------------------------
# Uses vision to confirm clearance. Does not click or type.
runtime.set_captcha_options(
CaptchaOptions(policy="callback", handler=VisionSolver())
)
# ---------------------------------------------------------------------
# Option 3: External resolver orchestration
# ---------------------------------------------------------------------
# Calls your webhook/service, then waits for clearance.
runtime.set_captcha_options(
CaptchaOptions(
policy="callback",
handler=ExternalSolver(lambda ctx: notify_webhook(ctx)),
timeout_ms=180_000, # 3 minutes for external service
)
)
# ---------------------------------------------------------------------
# Option 4: Abort policy (safest for production)
# ---------------------------------------------------------------------
# Immediately stop when CAPTCHA is detected. No resolution attempted.
runtime.set_captcha_options(
CaptchaOptions(policy="abort", min_confidence=0.7)
)
# Navigate and take snapshot (CAPTCHA handling triggers automatically)
await page.goto(os.environ.get("CAPTCHA_TEST_URL", "https://example.com"))
runtime.begin_step("Captcha-aware snapshot")
await runtime.snapshot()
if __name__ == "__main__":
asyncio.run(main())Predicate detects the following CAPTCHA providers with high confidence:
| Provider | provider_hint | Detection Signals |
|---|---|---|
| Google reCAPTCHA | recaptcha | iframe src, .g-recaptcha, [data-sitekey] |
| hCaptcha | hcaptcha | iframe src, .h-captcha |
| Cloudflare Turnstile | turnstile | iframe src, .cf-turnstile, [data-cf-turnstile-sitekey] |
| Arkose Labs (FunCaptcha) | arkose | iframe src, #FunCaptcha, [data-arkose-public-key] |
| AWS WAF CAPTCHA | awswaf | iframe src, [data-awswaf-captcha], script src |
| Generic/Unknown | unknown | Text keywords: "verify you are human", "unusual traffic", "security check" |
The snapshot.diagnostics.captcha object contains:
type CaptchaDiagnostics = {
detected: boolean;
provider_hint?: "recaptcha" | "hcaptcha" | "turnstile" | "arkose" | "awswaf" | "unknown";
confidence: number; // 0..1
evidence: {
text_hits: string[]; // Matched text keywords
selector_hits: string[]; // Matched CSS selectors
iframe_src_hits: string[]; // Matched iframe sources
url_hits: string[]; // Matched URL patterns
};
};
The detection uses a multi-signal scoring system:
A CAPTCHA is considered detected when confidence >= 0.7 (configurable via minConfidence).
If you integrate an external provider (e.g., 2captcha, Anti-Captcha) or your own internal system:
The external system performs the actual resolution. Predicate monitors for clearance.
Your handler receives a context object with:
| Field (Python) | Field (TypeScript) | Description |
|---|---|---|
run_id | runId | Current run identifier |
step_index | stepIndex | Current step number |
url | url | Page URL where CAPTCHA was detected |
captcha | captcha | CAPTCHA diagnostics (provider_hint, confidence, evidence) |
Keep audit logs and ensure your resolution approach complies with your organization's policies (consent, allowed domains, rate limits).
Your handler should return or allow the default wait_until_cleared action. The runtime then confirms clearance before resuming.
Set appropriate timeouts based on your external service's SLA. External services may take 30-180 seconds to resolve.
CAPTCHA events are automatically emitted as verification events to the tracer:
{
"type": "verification",
"data": {
"kind": "captcha",
"label": "captcha_detected",
"passed": false,
"reason": "CAPTCHA detected: recaptcha (confidence: 0.85)",
"details": {
"provider_hint": "recaptcha",
"confidence": 0.85,
"evidence": {
"selector_hits": [".g-recaptcha"],
"iframe_src_hits": ["https://www.google.com/recaptcha/..."]
}
}
},
"step_id": "abc-123"
}
policy: "abort" in production until you understand your CAPTCHA patterns