Solve captchas through the 2captcha API, not its extension

The two Tradeify automations clicked the 2captcha *extension's* solve button
and waited for it to go green. That needed a second extension installed and
signed in, put a real mouse click on a button whose markup is 2captcha's to
change, and gave a run no way to tell "still solving" from "not installed".

Replaced with one `solveCaptcha` step. No mouse: the extension reads the
widget's sitekey out of the page, the dashboard buys a token from 2captcha,
and the extension puts it in the response field and fires the page's own
callback.

captcha-hook.js is the part that earns its keep. A widget rendered
programmatically puts nothing in the DOM — no sitekey, and no callback. Most
pages only enable submit from that callback, so filling the hidden field alone
leaves the page looking unsolved. The hook wraps turnstile/grecaptcha/hcaptcha
`render` at document_start to hold on to what the page passed.

Solves go through 2captcha's residential proxy. Its API rejects host names in
proxyAddress, so na.proxy.2captcha.com is resolved to an IP first (cached);
the gateway was confirmed to work addressed that way.

Covers reCAPTCHA v2 (incl. invisible and Enterprise), hCaptcha and Turnstile.
NOT reCAPTCHA v3: a v3 page carries an anchor iframe with k=, so it is
detected as v2 and would be solved with the wrong task type. Everything else
unsupported fails loudly instead.

The API key and proxy credentials are hardcoded in the captcha route on
request. Unlike the sqlite database, that file is in the repo, so they travel
with it. TWOCAPTCHA_API_KEY / TWOCAPTCHA_PROXY and the settings rows still
override; neither has a field on the Settings page.

Runner VERSION and RUNNER_EXPECTED_VERSION both to 0.17.0 — they are a pair,
and a mismatch shows as a red "restart your runner" banner that blames the
wrong process.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon Li
2026-08-31 00:18:00 -05:00
co-authored by Claude Opus 5
parent 25289cb9b3
commit 159389be35
11 changed files with 837 additions and 28 deletions
+11 -2
View File
@@ -32,7 +32,7 @@ captures the tab you are on.
## Scope
Capture has no switch, so scope is what keeps it honest. Two limits, both
Capture has no switch, so scope is what keeps it honest. Three limits, all
enforced by Chrome rather than by this code:
1. **`host_permissions` is deliberately narrow** — the firm sites plus localhost
@@ -43,11 +43,20 @@ enforced by Chrome rather than by this code:
last-focused window *and* a host_permissions match. A firm tab sitting in the
background is not captured, and neither is anything else. Switch away and
capture simply stops until you switch back.
3. **One content script, same hosts.** `captcha-hook.js` runs at document_start
on the firm sites only. It wraps `turnstile.render` / `grecaptcha.render` /
`hcaptcha.render` and remembers what the page passed them — the sitekey, and
above all the callback a solved token has to be handed to, which a
programmatically rendered widget puts nowhere else. It reads nothing else and
sends nothing anywhere; the remembered arguments stay in the page's own world
until a `solveCaptcha` step comes to collect them.
To automate a different broker, add its pattern to `host_permissions` in
`manifest.json` and reload the extension — that alone scopes capture to it. Avoid
going back to `<all_urls>`: it would put every tab you visit in reach, and limit
1 is the only thing standing between this and scraping your whole browser.
1 is the only thing standing between this and scraping your whole browser. A new
firm needs its host in *both* lists — `host_permissions` and the `content_scripts`
matches — or captchas on that site will have no callback to fire.
## Notes