# VanishCaptcha — technical brief

> **Learning / test project — not a live product.** Architecture notes for a personal project exploring AI usage platforms. Not a design for a live commercial service.

How a client asks for a solve, how the server produces a token, and what v1 does.

**Success metric:** the page that sent `sitekey` / `url` / `user_agent` / `rqdata` accepts the returned token. A token string from the solver is not enough on its own.

**v1 scope:** 9-tile (3×3) hCaptcha `image_grid` only. Other widget types fail as `unsupported_challenge`. How to add them: [solver-teams/extending-challenge-types.md](solver-teams/extending-challenge-types.md).

---

## 1. Architecture

**VanishCaptcha** is a test-bed hCaptcha token API. Any client that can POST `/v1/solves` and later submit the token on the **same origin** can use it. Page-specific clicks stay in the client. This is for learning, not a live product.

```
┌─────────────────────────────────────┐         ┌──────────────────────────────────────┐
│  Client (real site tab)             │         │  Solver farm (separate IPs)          │
│                                     │         │                                      │
│  Content script  ──message──►       │  HTTPS  │  API  →  queue  →  solver browser    │
│  Background (proxy + API key)  ─────┼────────►│              ↓                       │
│  Submit token on the real page      │  token  │         trained AI model             │
│                                     │ ◄───────┤              ↓                       │
│                                     │         │         click tiles → token          │
└─────────────────────────────────────┘         └──────────────────────────────────────┘
        real protected site                       never opens the client’s real site
```

| Component | What it does |
| --- | --- |
| **Client** | Detects hCaptcha, sends ticket fields, waits for a token, submits it on the real page. |
| **API** | HTTP `/v1/solves`. Auth, job id, long-poll. No browser, no model. |
| **Queue** | Durable job record + work queues so API, solver browser, and model can scale separately. |
| **Solver browser** | Fresh hCaptcha widget with the client’s sitekey, origin, user-agent, and optional `rqdata`. Clicks tiles on **that** session. |
| **Model** | Chooses which of the 9 tiles to click. Does not run in the client tab. |

Solver machines and client browsers use **different IPs**. The solver browser must not load the client’s real site. It presents the client **hostname** from `url` so the token is origin-bound.

### Features in place today

- Create/poll API (`POST` / `GET` `/v1/solves`), Bearer key, job ids `solve_…`
- Long-poll up to 90 seconds
- Enterprise challenges (`rqdata`) treated as `hcaptcha_enterprise`
- Isolated widget that presents the client **origin** without opening the real page
- Multi-round **9-tile** grids (Next → … → Verify)
- Text-only grids and reference-image grids, routed automatically
- Resume after tab reload (job id stored, then `GET` again)
- Kill switch (`MAINTENANCE`) fails new jobs

---

## 2. How the client sends a solve request

The page script **cannot** call the solver directly (CORS). A privileged background / server worker fetches the solver host and attaches the API key.

### When it fires

1. The real page shows an hCaptcha widget (or a WAF overlay that embeds one).
2. The client reads **sitekey** from the iframe (or from hCaptcha config).
3. If present, it also reads **rqdata** (enterprise payload).
4. It starts one in-flight solve (duplicates are ignored).

### What it sends

`POST {solverUrl}/v1/solves?wait=90` with:

```http
Authorization: Bearer <api key>
Content-Type: application/json
```

```json
{
  "type": "hcaptcha",
  "sitekey": "<from the hCaptcha iframe>",
  "url": "<window.location.href of the real tab>",
  "user_agent": "<navigator.userAgent of that browser>",
  "rqdata": "<only if the challenge provided it>"
}
```

If `rqdata` is present, the server treats the job as **enterprise** even if `type` said `hcaptcha`.

These four fields are what hCaptcha binds the token to. They must match the tab that will consume the token.

### How it waits

- If the POST already returns `status: "solved"` and a `token`, use it immediately.
- Otherwise keep `id` (e.g. `solve_…`) and poll `GET /v1/solves/{id}?wait=10`.
- HTTP **202** or `pending` / `solving` means “keep polling”.
- If the tab reloads, resume the same `id` (stale ids are dropped).
- If the job is missing/expired, create a **new** POST.

The client does not click tiles and does not open a second hCaptcha widget for the solver.

---

## 3. How the server solves it

### API

1. Checks the Bearer key (hashed at rest; lookup by key prefix).
2. Rejects the request if the service is in maintenance or the key is over rate/concurrency limits.
3. Creates a job record (`pending` → later `solving` / `solved` / `failed`).
4. Enqueues **acquire** work and **long-polls** until the wait budget expires or the job finishes.
5. Returns JSON (`id`, `status`, `token`, `error`).

The API process never opens Chromium and never runs the model.

### Solver browser (acquire)

1. New isolated context; user-agent set to the string from the client.
2. Opens an **isolated harness**, not the real page in `url`.
3. Network policy: allow hCaptcha; do not load the client site; present that **hostname** so the widget is origin-bound.
4. Injects `sitekey` and `rqdata` into that widget.
5. Waits for a live **9-tile** `image_grid`.

If the widget never appears, the job fails (`invalid_sitekey` / `challenge_timeout`). If the grid is not 9 tiles, or not `image_grid`, the job fails (`unsupported_challenge`). Screenshots of the **client’s** iframe are not used.

### Model (infer)

For each 9-tile round:

1. The solver browser saves that round’s tiles (and a small reference image when the prompt has a header picture).
2. The model scores tiles against the instruction (text-only) or against the reference picture.
3. It returns tile indexes (and a confidence). Below threshold → fail closed, no random clicks.
4. Later rounds call the model again on the **same** session.

### Click and mint token (submit)

On the **same** widget:

1. Click the chosen tiles.
2. Press the submit control: **Next** (new 9-tile grid) or **Verify** (final check).
3. Repeat until a pass token appears (`window._token`) or the round limit is hit.
4. Write `status: "solved"` and `token` on the job.

If that browser process dies mid-job, the session is lost. Old screenshots cannot mint a valid token.

Typical live challenge: several 9-tile rounds, then one Verify.

---

## 4. How the client uses the token

Back on the **real page** (top frame, not the hCaptcha iframe):

1. Receive `token`.
2. Submit it the way that site expects (`h-captcha-response` / `g-recaptcha-response` / site-specific POST).
3. Treat **the site’s accept** as success, not merely a non-empty token string.

The checkbox on the real page may stay unchecked. That is expected: tiles were clicked on the **solver** widget.

---

## 5. Why this design

| Constraint | Solution |
| --- | --- |
| Tokens are bound to UA, host, rqdata, and a live widget session | Copy those fields; solve on a fresh widget; never reuse old screenshots |
| The client tab must keep doing its own work | Solver farm is a separate browser; client only waits on HTTP |
| CORS blocks the page script from the solver host | Background / privileged proxy with host permission + Bearer key |
| Grids are multi-round | Model + clicks loop until Verify |
| Two visual layouts | Same 9-tile pipeline; route text vs reference from the prompt |

---

## 6. What v1 does not do

- Solve inside the client tab or share client IPs with the solver farm
- Open or fill the client’s real site in the solver browser
- Treat a returned token as success without the client page accepting it
- Store tokens, API keys, or user identifiers in challenge artifacts
- Drag, canvas, area-select, 4×4, or other non-9-tile families (see [extending-challenge-types.md](solver-teams/extending-challenge-types.md))
- GeeTest
- A live product, paying customers, or production traffic
