diff --git a/captcha-third-party-services/SKILL.md b/captcha-third-party-services/SKILL.md new file mode 100644 index 0000000..1073050 --- /dev/null +++ b/captcha-third-party-services/SKILL.md @@ -0,0 +1,140 @@ +--- +name: captcha-third-party-services +description: Unified captcha workflow for 2Captcha, YesCaptcha, and Anti-Captcha. Use when users need official API-based task submission (`createTask`), polling (`getTaskResult`), balance checks (`getBalance`), provider fallback, or debugging captcha token pipelines for reCAPTCHA/hCaptcha/Turnstile/FunCaptcha and similar challenge types. +--- + +# Captcha Third Party Services + +## Overview + +Use official APIs to create captcha tasks, poll completion, and return solve tokens with consistent behavior across providers. + +Read `references/official-docs-and-analysis.md` when you need provider-specific details, method URLs, and compatibility notes. + +## Inputs To Collect + +- Provider: `2captcha`, `yescaptcha`, or `anti-captcha` +- Captcha task type (for example `RecaptchaV2TaskProxyless`, `TurnstileTaskProxyless`, `HCaptchaTaskProxyless`) +- Target metadata: `websiteURL`, `websiteKey` and task-specific fields +- API key (runtime secret, not hardcoded into committed files) + +## API Key Handling + +Prefer environment variables: + +- `CAPTCHA_2CAPTCHA_KEY` +- `CAPTCHA_YESCAPTCHA_KEY` +- `CAPTCHA_ANTI_CAPTCHA_KEY` + +Only use direct `--api-key` arguments for one-off tests. + +## Unified Workflow + +1. Check balance before creating tasks. +2. Build a `task` payload that matches provider-supported task schema. +3. Submit `createTask`. +4. Poll `getTaskResult` every 3-5 seconds until `status=ready` or timeout. +5. Return `solution` token and timing/cost metadata. + +## Provider Selection Strategy + +- Start with preferred provider from user. +- If provider returns permanent task validation errors, fix payload first. +- If provider returns transient capacity/timeouts, fail over to next provider. +- Keep task type and site parameters identical during failover to isolate provider variance. + +Recommended fallback order: + +1. `yescaptcha` +2. `2captcha` +3. `anti-captcha` + +Adjust order using account balance, measured solve latency, and recent success rate. + +## Use The Bundled CLI + +Use `scripts/captcha_api_cli.py` for deterministic API calls. + +### Balance + +```bash +python3 scripts/captcha_api_cli.py balance --provider 2captcha +python3 scripts/captcha_api_cli.py balance --provider yescaptcha +python3 scripts/captcha_api_cli.py balance --provider anti-captcha +``` + +### Create Task + +```bash +python3 scripts/captcha_api_cli.py create-task \ + --provider 2captcha \ + --task-json '{"type":"RecaptchaV2TaskProxyless","websiteURL":"https://example.com","websiteKey":"SITE_KEY"}' +``` + +### Poll Task Result + +```bash +python3 scripts/captcha_api_cli.py get-task-result \ + --provider 2captcha \ + --task-id 123456789 +``` + +### End-To-End Solve (Create + Poll) + +```bash +python3 scripts/captcha_api_cli.py solve \ + --provider yescaptcha \ + --task-json '{"type":"TurnstileTaskProxyless","websiteURL":"https://example.com","websiteKey":"SITE_KEY"}' \ + --poll-interval 3 \ + --timeout 180 +``` + +## Raw Curl Patterns + +Use these when a user explicitly asks for direct HTTP examples. + +### createTask + +```bash +curl -sS https://api./createTask \ + -H 'Content-Type: application/json' \ + -d '{ + "clientKey":"", + "task":{ + "type":"RecaptchaV2TaskProxyless", + "websiteURL":"https://example.com", + "websiteKey":"SITE_KEY" + } + }' +``` + +### getTaskResult + +```bash +curl -sS https://api./getTaskResult \ + -H 'Content-Type: application/json' \ + -d '{ + "clientKey":"", + "taskId":123456789 + }' +``` + +### getBalance + +```bash +curl -sS https://api./getBalance \ + -H 'Content-Type: application/json' \ + -d '{"clientKey":""}' +``` + +## Troubleshooting + +- `errorId != 0`: treat as API-level failure and inspect `errorCode`/`errorDescription`. +- Stuck in `processing`: extend timeout or switch provider. +- Invalid key/site params: validate task type and required fields from official docs. +- Low balance: call `getBalance` and select another provider if needed. + +## Compliance + +- Use these services only for authorized security testing and legitimate automation. +- Respect target website Terms of Service and applicable laws. diff --git a/captcha-third-party-services/agents/openai.yaml b/captcha-third-party-services/agents/openai.yaml new file mode 100644 index 0000000..6cfa948 --- /dev/null +++ b/captcha-third-party-services/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Captcha Third-Party Services" + short_description: "Unified workflows for 2Captcha, YesCaptcha, Anti-Captcha" + default_prompt: "Use official APIs to create captcha tasks, poll results, and manage provider fallback across 2Captcha, YesCaptcha, and Anti-Captcha." diff --git a/captcha-third-party-services/references/official-docs-and-analysis.md b/captcha-third-party-services/references/official-docs-and-analysis.md new file mode 100644 index 0000000..33f41d8 --- /dev/null +++ b/captcha-third-party-services/references/official-docs-and-analysis.md @@ -0,0 +1,96 @@ +# Official Docs And Analysis + +This file summarizes official API documentation for 2Captcha, YesCaptcha, and Anti-Captcha and maps them into one operational workflow. + +## Official Sources + +### 2Captcha + +- API docs hub: https://2captcha.com/api-docs +- `createTask`: https://2captcha.com/api-docs/create-task +- `getTaskResult`: https://2captcha.com/api-docs/get-task-result +- `getBalance`: https://2captcha.com/api-docs/get-balance + +### YesCaptcha + +- Dashboard: https://yescaptcha.com/dashboard.html +- `createTask`: https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/7930129/createTask+submit+a+captcha+task +- `getTaskResult`: https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/7930006/getTaskResult+Task+Result+Method +- `getBalance`: https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/7930094/getBalance+balance+Method + +### Anti-Captcha + +- API docs hub: https://anti-captcha.com/apidoc +- `createTask`: https://anti-captcha.com/apidoc/methods/createTask +- `getTaskResult`: https://anti-captcha.com/apidoc/methods/getTaskResult +- `getBalance`: https://anti-captcha.com/apidoc/methods/getBalance + +## Endpoint Matrix + +All three providers use `POST` + JSON and share the same core method names. + +| Provider | createTask | getTaskResult | getBalance | +|---|---|---|---| +| 2Captcha | `https://api.2captcha.com/createTask` | `https://api.2captcha.com/getTaskResult` | `https://api.2captcha.com/getBalance` | +| YesCaptcha | `https://api.yescaptcha.com/createTask` | `https://api.yescaptcha.com/getTaskResult` | `https://api.yescaptcha.com/getBalance` | +| Anti-Captcha | `https://api.anti-captcha.com/createTask` | `https://api.anti-captcha.com/getTaskResult` | `https://api.anti-captcha.com/getBalance` | + +## Shared Request Pattern + +### createTask + +```json +{ + "clientKey": "YOUR_API_KEY", + "task": { + "type": "RecaptchaV2TaskProxyless", + "websiteURL": "https://example.com", + "websiteKey": "SITE_KEY" + } +} +``` + +### getTaskResult + +```json +{ + "clientKey": "YOUR_API_KEY", + "taskId": 123456789 +} +``` + +### getBalance + +```json +{ + "clientKey": "YOUR_API_KEY" +} +``` + +## Shared Response Pattern + +- `errorId = 0`: request accepted/successful. +- `errorId != 0`: inspect `errorCode` and `errorDescription`. +- `getTaskResult.status`: + - `processing` + - `ready` with `solution`. + +## Practical Differences + +- **Domain and account pool differ**: task acceptance, queue depth, and solve latency vary by provider. +- **Balance shape differs slightly**: + - 2Captcha and Anti-Captcha commonly return `balance`. + - YesCaptcha may also return additional balance fields (for example `softBalance` and invite-related fields). +- **Task support overlap is high but not perfect**: always verify task type and required fields per provider docs before failover. + +## Integration Recommendations + +1. Keep one canonical `task` payload schema in your project. +2. Swap only provider domain + API key for failover. +3. Poll every 3-5 seconds; stop on timeout and switch provider. +4. Log provider, task type, taskId, latency, and errorCode for reliability tuning. +5. Never hardcode API keys into version-controlled files. + +## Live Connectivity Check (2026-03-03) + +`getBalance` was successfully called for all three providers (`errorId: 0`) during skill implementation on March 3, 2026. diff --git a/captcha-third-party-services/scripts/captcha_api_cli.py b/captcha-third-party-services/scripts/captcha_api_cli.py new file mode 100755 index 0000000..9347226 --- /dev/null +++ b/captcha-third-party-services/scripts/captcha_api_cli.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +"""Unified CLI for 2Captcha, YesCaptcha, and Anti-Captcha API methods.""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import Any, Dict + + +PROVIDERS = { + "2captcha": { + "endpoint": "https://api.2captcha.com", + "env_var": "CAPTCHA_2CAPTCHA_KEY", + }, + "yescaptcha": { + "endpoint": "https://api.yescaptcha.com", + "env_var": "CAPTCHA_YESCAPTCHA_KEY", + }, + "anti-captcha": { + "endpoint": "https://api.anti-captcha.com", + "env_var": "CAPTCHA_ANTI_CAPTCHA_KEY", + }, +} + +PROVIDER_ALIASES = { + "2captcha": "2captcha", + "2-captcha": "2captcha", + "yescaptcha": "yescaptcha", + "yes-captcha": "yescaptcha", + "anticaptcha": "anti-captcha", + "anti-captcha": "anti-captcha", +} + + +class CliError(Exception): + """Raised for expected CLI errors.""" + + +def normalize_provider(provider: str) -> str: + key = provider.strip().lower() + normalized = PROVIDER_ALIASES.get(key) + if not normalized: + supported = ", ".join(sorted(PROVIDERS)) + raise CliError(f"Unsupported provider '{provider}'. Supported: {supported}") + return normalized + + +def resolve_api_key(provider: str, explicit_key: str | None) -> str: + if explicit_key: + return explicit_key + env_var = PROVIDERS[provider]["env_var"] + value = os.getenv(env_var, "").strip() + if value: + return value + raise CliError( + f"Missing API key for provider '{provider}'. " + f"Set --api-key or environment variable {env_var}." + ) + + +def call_api(provider: str, method: str, payload: Dict[str, Any], timeout: int = 60) -> Dict[str, Any]: + base = PROVIDERS[provider]["endpoint"] + url = f"{base}/{method}" + body = json.dumps(payload).encode("utf-8") + request = urllib.request.Request( + url=url, + data=body, + headers={"Content-Type": "application/json"}, + method="POST", + ) + + try: + with urllib.request.urlopen(request, timeout=timeout) as response: + raw = response.read().decode("utf-8") + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + raise CliError(f"HTTP {exc.code} from {url}: {detail}") from exc + except urllib.error.URLError as exc: + raise CliError(f"Network error calling {url}: {exc}") from exc + + try: + data = json.loads(raw) + except json.JSONDecodeError as exc: + raise CliError(f"Invalid JSON response from {url}: {raw}") from exc + + return data + + +def ensure_api_success(response: Dict[str, Any]) -> None: + error_id = response.get("errorId") + if error_id in (0, "0", None): + return + + code = response.get("errorCode", "UNKNOWN") + desc = response.get("errorDescription", "") + message = f"Provider API error (errorId={error_id}, errorCode={code})" + if desc: + message = f"{message}: {desc}" + raise CliError(message) + + +def load_task_payload(task_json: str | None, task_file: str | None) -> Dict[str, Any]: + if not task_json and not task_file: + raise CliError("Provide either --task-json or --task-file.") + + if task_json and task_file: + raise CliError("Use only one of --task-json or --task-file.") + + if task_json: + source = task_json + else: + path = Path(task_file or "") + if not path.exists(): + raise CliError(f"Task file does not exist: {path}") + source = path.read_text() + + try: + task = json.loads(source) + except json.JSONDecodeError as exc: + raise CliError(f"Invalid task JSON: {exc}") from exc + + if not isinstance(task, dict): + raise CliError("Task JSON must be an object.") + if "type" not in task: + raise CliError("Task JSON must include a 'type' field.") + return task + + +def print_json(data: Dict[str, Any]) -> None: + print(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True)) + + +def cmd_balance(args: argparse.Namespace) -> int: + provider = normalize_provider(args.provider) + key = resolve_api_key(provider, args.api_key) + payload = {"clientKey": key} + result = call_api(provider, "getBalance", payload) + ensure_api_success(result) + print_json({"provider": provider, "result": result}) + return 0 + + +def cmd_create_task(args: argparse.Namespace) -> int: + provider = normalize_provider(args.provider) + key = resolve_api_key(provider, args.api_key) + task = load_task_payload(args.task_json, args.task_file) + + payload: Dict[str, Any] = {"clientKey": key, "task": task} + if args.soft_id is not None: + payload["softId"] = args.soft_id + + result = call_api(provider, "createTask", payload) + ensure_api_success(result) + print_json({"provider": provider, "result": result}) + return 0 + + +def cmd_get_task_result(args: argparse.Namespace) -> int: + provider = normalize_provider(args.provider) + key = resolve_api_key(provider, args.api_key) + payload = {"clientKey": key, "taskId": args.task_id} + + result = call_api(provider, "getTaskResult", payload) + ensure_api_success(result) + print_json({"provider": provider, "result": result}) + return 0 + + +def cmd_solve(args: argparse.Namespace) -> int: + provider = normalize_provider(args.provider) + key = resolve_api_key(provider, args.api_key) + task = load_task_payload(args.task_json, args.task_file) + + create_payload: Dict[str, Any] = {"clientKey": key, "task": task} + if args.soft_id is not None: + create_payload["softId"] = args.soft_id + + create_result = call_api(provider, "createTask", create_payload) + ensure_api_success(create_result) + + task_id = create_result.get("taskId") + if not task_id: + raise CliError(f"createTask succeeded but taskId missing: {create_result}") + + deadline = time.time() + args.timeout + + while time.time() < deadline: + poll_payload = {"clientKey": key, "taskId": task_id} + poll_result = call_api(provider, "getTaskResult", poll_payload) + ensure_api_success(poll_result) + + status = poll_result.get("status") + if status == "ready": + print_json( + { + "provider": provider, + "taskId": task_id, + "createTask": create_result, + "getTaskResult": poll_result, + } + ) + return 0 + + if status != "processing": + raise CliError(f"Unexpected getTaskResult status '{status}': {poll_result}") + + time.sleep(args.poll_interval) + + raise CliError( + f"Timeout waiting for task {task_id} after {args.timeout} seconds " + f"(poll interval {args.poll_interval}s)." + ) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Unified captcha API CLI for 2Captcha, YesCaptcha, Anti-Captcha." + ) + subparsers = parser.add_subparsers(dest="command", required=True) + + p_balance = subparsers.add_parser("balance", help="Call getBalance") + p_balance.add_argument("--provider", required=True, help="2captcha|yescaptcha|anti-captcha") + p_balance.add_argument("--api-key", help="Provider API key (optional if env var is set)") + p_balance.set_defaults(func=cmd_balance) + + p_create = subparsers.add_parser("create-task", help="Call createTask") + p_create.add_argument("--provider", required=True, help="2captcha|yescaptcha|anti-captcha") + p_create.add_argument("--api-key", help="Provider API key (optional if env var is set)") + p_create.add_argument("--task-json", help="Inline JSON object for 'task'") + p_create.add_argument("--task-file", help="Path to JSON file containing 'task' object") + p_create.add_argument("--soft-id", type=int, help="Optional softId value") + p_create.set_defaults(func=cmd_create_task) + + p_result = subparsers.add_parser("get-task-result", help="Call getTaskResult") + p_result.add_argument("--provider", required=True, help="2captcha|yescaptcha|anti-captcha") + p_result.add_argument("--api-key", help="Provider API key (optional if env var is set)") + p_result.add_argument("--task-id", required=True, type=int, help="Task ID from createTask") + p_result.set_defaults(func=cmd_get_task_result) + + p_solve = subparsers.add_parser("solve", help="Create task and poll until ready") + p_solve.add_argument("--provider", required=True, help="2captcha|yescaptcha|anti-captcha") + p_solve.add_argument("--api-key", help="Provider API key (optional if env var is set)") + p_solve.add_argument("--task-json", help="Inline JSON object for 'task'") + p_solve.add_argument("--task-file", help="Path to JSON file containing 'task' object") + p_solve.add_argument("--soft-id", type=int, help="Optional softId value") + p_solve.add_argument("--poll-interval", type=int, default=3, help="Polling interval in seconds") + p_solve.add_argument("--timeout", type=int, default=180, help="Solve timeout in seconds") + p_solve.set_defaults(func=cmd_solve) + + return parser + + +def main() -> int: + parser = build_parser() + args = parser.parse_args() + try: + return args.func(args) + except CliError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main())