autoreg-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
506
skills/grok-autoreg/SKILL.md
Normal file
506
skills/grok-autoreg/SKILL.md
Normal file
@@ -0,0 +1,506 @@
|
||||
---
|
||||
name: grok-autoreg
|
||||
description: Use when registering Grok/x.ai accounts in bulk.
|
||||
---
|
||||
|
||||
# Grok/x.ai Auto-Registration
|
||||
|
||||
## Trigger
|
||||
Mass-registering Grok (x.ai) accounts, extracting API tokens, or managing the Grok token pool.
|
||||
|
||||
## CRITICAL: Search Before Writing
|
||||
|
||||
Before writing ANY custom registration script:
|
||||
1. **Search GitHub** for existing solutions — search `grok register`, `grok autoreg`, `grok2api` on GitHub
|
||||
2. **Search the user's PC** for ready-made tools — check `~/Documents/`, `~/Desktop/`, `~/projects/` for directories like `*grok*`, `*gw*`, `*gateway*`, `*2api*`
|
||||
3. **Check if the gw46.py gateway exists** at `C:\Users\User\Documents\grok46gw/gw46.py` — it's a complete Grok API gateway with OAuth auto-registration, device code flow, and SQLite account storage
|
||||
|
||||
**Existing ready-made tools found on this PC:**
|
||||
- `C:\Users\User\Documents\grok46gw/gw46.py` — Grok 4.6 Gateway (FastAPI, OAuth, device code flow, SQLite)
|
||||
- `C:\Users\User\Desktop\free2api/` — Node.js API gateway (Freemodel/OpenAI proxy)
|
||||
- `C:\Users\User\Desktop\ready_grok_reg/` — Cloned from ReinerBRO/grok-register (DrissionPage-based, Turnstile shadow DOM bypass)
|
||||
|
||||
**Existing GitHub repos:**
|
||||
- **ReinerBRO/grok-register** (398 ★) — DrissionPage-based, Chrome extension for Turnstile bypass, DuckMail temp emails, auto-push to grok2api
|
||||
- **xinxinshuhao-create/grok-register** (48 ★) — curl_cffi-based, YesCaptcha, multi-provider email support, device flow for token minting
|
||||
|
||||
## Turnstile Bypass: Shadow DOM Technique (Best Approach Found)
|
||||
|
||||
The DrissionPage-based approach (ReinerBRO/grok-register) uses a Chrome extension + shadow DOM access to bypass Turnstile WITHOUT 2captcha:
|
||||
|
||||
1. The Turnstile widget container has a **shadow root** with an iframe inside
|
||||
2. The script accesses the shadow root via `element.shadow_root`
|
||||
3. Finds the iframe and injects JavaScript to patch `MouseEvent.screenX/screenY`
|
||||
4. Clicks the challenge button inside the iframe
|
||||
5. The patched mouse event coordinates make Cloudflare's behavioral detection pass
|
||||
6. The Turnstile auto-solves without user interaction
|
||||
|
||||
**This is more reliable than 2captcha** — it doesn't depend on external captcha solving services and works on both the code-page Turnstile (sitekey `0x4AAAAAAADnQ4F4U8W2nQyH`) and the profile-page Turnstile (sitekey `0x4AAAAAAAhr9JGVDZbrZOo0`).
|
||||
|
||||
**Key code pattern** (from DrissionPage_example.py):
|
||||
```python
|
||||
challengeSolution = page.ele("@name=cf-turnstile-response")
|
||||
challengeWrapper = challengeSolution.parent()
|
||||
challengeIframe = challengeWrapper.shadow_root.ele("tag:iframe")
|
||||
challengeIframe.run_js("""
|
||||
// Patch MouseEvent to bypass Cloudflare behavioral detection
|
||||
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX });
|
||||
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY });
|
||||
""")
|
||||
challengeIframeBody = challengeIframe.ele("tag:body").shadow_root
|
||||
challengeButton = challengeIframeBody.ele("tag:input")
|
||||
challengeButton.click()
|
||||
```
|
||||
|
||||
## x-challenge Cookie Is NOT a Valid API Token
|
||||
|
||||
The `x-challenge` and `x-signature` cookies set by `grok.com` are **Cloudflare challenge cookies**, NOT valid API authentication tokens. When captured as "SSO tokens", they will fail with `"Incorrect API key provided"` when used with `api.x.ai/v1`.
|
||||
|
||||
**Valid tokens are obtained via:**
|
||||
1. **OAuth device code flow** (via gw46.py gateway or `grok login --device-auth`)
|
||||
2. **Direct OAuth PKCE flow** (via gw46.py `/oauth/login` endpoint)
|
||||
3. **The `auth.json` file** at `~/.grok/auth.json` (written by `grok login --oauth` or `grok login --device-auth`)
|
||||
|
||||
## Ready-Made Gateway: gw46.py
|
||||
|
||||
Location: `C:\Users\User\Documents\grok46gw/gw46.py`
|
||||
|
||||
**Features:**
|
||||
- OpenAI-compatible API (`/v1/chat/completions`, `/v1/responses`)
|
||||
- Multi-model support: grok-4.6, grok-4.5, grok-4.3, grok-build-0.1
|
||||
- OAuth auto-registration with PKCE flow (`/oauth/login`)
|
||||
- Device code flow (`/oauth/device`, `/oauth/device/poll`)
|
||||
- SQLite account database (`grok46_accounts.db`)
|
||||
- API key pool with round-robin rotation
|
||||
- Token refresh support
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
cd ~/Documents/grok46gw
|
||||
python gw46.py # starts on 127.0.0.1:20146
|
||||
```
|
||||
|
||||
**Getting tokens via device code flow:**
|
||||
```
|
||||
GET http://127.0.0.1:20146/oauth/device → returns user_code + verification_uri
|
||||
User opens verification_uri, signs in, enters user_code, authorizes
|
||||
GET http://127.0.0.1:20146/oauth/device/poll → returns token
|
||||
```
|
||||
|
||||
## DuckMail: Primary Temp Email Provider (2026-08-13)
|
||||
|
||||
**DuckMail** (`api.duckmail.sbs`) is the preferred temporary email provider. It is a **mail.tm-compatible API** that works **without any bearer token** and has **no practical rate limits** (19 domains, rotates automatically).
|
||||
|
||||
### Why DuckMail Over mail.tm
|
||||
|
||||
| Feature | DuckMail | mail.tm |
|
||||
|---------|----------|---------|
|
||||
| Bearer token required | **No** | No |
|
||||
| Rate limit | **None observed** | 429 after 3-4/min |
|
||||
| Available domains | **19** (growing) | ~5 |
|
||||
| API compatibility | mail.tm-compatible | Native |
|
||||
| Observed success rate | **~95%** | ~30% under load |
|
||||
|
||||
### Switching from mail.tm to DuckMail
|
||||
Change the API base URL in `email_service.py`:
|
||||
```python
|
||||
MAILTM_BASE = "https://api.duckmail.sbs" # was "https://api.mail.tm"
|
||||
```
|
||||
All mail.tm-compatible code works unchanged.
|
||||
|
||||
### Available Domains (2026-08-13)
|
||||
duckmail.sbs, niceground.shop, stoneground.shop, lakeground.shop, canvaspace.shop, vercelspace.shop, seedancespace.shop, happyhorsespace.shop, bananaspace.shop, sunstarmoon.shop, moonstarsun.shop, sunmoonlight.shop, makesomestone.shop, mikesomelike.shop, somestoneair.shop, hubaiclass.org, markaihub.shop, markstonehub.org, glasswhitehub.com
|
||||
|
||||
## PRIMARY Working Method: curl_cffi + gRPC-web (GrokRegister)
|
||||
|
||||
**Location**: `C:\Users\User\tmp\grok_register\grok-register\grok.py`
|
||||
|
||||
This is the **only consistently working approach** as of 2026-08-13. Uses `curl_cffi` (TLS fingerprint emulation) and direct gRPC-web API calls — no browser needed.
|
||||
|
||||
### Flow
|
||||
1. **Initialize**: Fetch sign-up page, extract `action_id` from JS chunks, `site_key` and `state_tree` from HTML
|
||||
2. **Create email**: DuckMail API → fresh disposable email
|
||||
3. **Send OTP**: gRPC-web call to `auth_mgmt.AuthManagement/CreateEmailValidationCode`
|
||||
4. **Wait for OTP**: Poll DuckMail inbox for verification code
|
||||
5. **Solve Turnstile**: 2captcha (`TurnstileTaskProxyless`)
|
||||
6. **Submit registration**: POST `/sign-up` with `next-action` header, OTP, Turnstile token, account details
|
||||
7. **Extract SSO**: Parse response for `set-cookie?q=` URL, follow it, capture `sso` cookie
|
||||
8. **Save**: Write SSO token to `keys/grok.txt` and account to `keys/accounts.txt`
|
||||
|
||||
### Key Code
|
||||
```python
|
||||
# gRPC-web message encoding
|
||||
def encode_grpc_message(field_id, string_value):
|
||||
key = (field_id << 3) | 2
|
||||
value_bytes = string_value.encode('utf-8')
|
||||
length = len(value_bytes)
|
||||
payload = struct.pack('B', key) + struct.pack('B', length) + value_bytes
|
||||
return b'\x00' + struct.pack('>I', len(payload)) + payload
|
||||
|
||||
# Send OTP via gRPC-web
|
||||
session.post(f"{site_url}/auth_mgmt.AuthManagement/CreateEmailValidationCode",
|
||||
data=encode_grpc_message(1, email),
|
||||
headers={"content-type": "application/grpc-web+proto", "x-grpc-web": "1",
|
||||
"x-user-agent": "connect-es/2.1.1"})
|
||||
|
||||
# Submit registration with Next.js server action
|
||||
headers = {
|
||||
"next-action": action_id, # from JS chunks
|
||||
"next-router-state-tree": state_tree, # from HTML
|
||||
"accept": "text/x-component",
|
||||
}
|
||||
payload = [{
|
||||
"emailValidationCode": otp_code,
|
||||
"createUserAndSessionRequest": {
|
||||
"email": email, "givenName": "Test", "familyName": "User",
|
||||
"clearTextPassword": password, "tosAcceptedVersion": "$undefined"
|
||||
},
|
||||
"turnstileToken": token, "promptOnDuplicateEmail": True
|
||||
}]
|
||||
session.post(f"{site_url}/sign-up", json=payload, headers=headers)
|
||||
```
|
||||
|
||||
### Usage
|
||||
```bash
|
||||
cd /c/Users/User/tmp/grok_register/grok-register
|
||||
export TWOCAPTCHA_API_KEY="[REDACTED-KEY]"
|
||||
python3 grok.py --threads 8 --count 100
|
||||
```
|
||||
|
||||
### Performance
|
||||
- **~2.6 seconds per account** with 8 threads
|
||||
- **~95% success rate** (DuckMail, no rate limit)
|
||||
- **No browser needed** — headless, curl_cffi for TLS fingerprinting
|
||||
|
||||
### Files
|
||||
- `grok.py` — Main script (thread pool, action ID, registration flow)
|
||||
- `email_service.py` — mail.tm/DuckMail API client
|
||||
- `YesCaptcha_service.py` — 2captcha Turnstile solver
|
||||
- `keys/grok.txt` — SSO tokens (one per line)
|
||||
- `keys/accounts.txt` — Account details (email:password:sso)
|
||||
|
||||
## SSO Token → OAuth Token Conversion (Device Auth Flow)
|
||||
|
||||
SSO cookies from GrokRegister are **NOT** valid API keys for `api.x.ai/v1`. They return `"Incorrect API key provided"`. To get OAuth tokens that work with the API gateway, use the **device auth flow** with DrissionPage to auto-authorize each SSO session.
|
||||
|
||||
### CRITICAL: Accounts Have No API Credits
|
||||
|
||||
Even after successful SSO → OAuth conversion, the accounts **have no API credits**. The x.ai API returns `402 personal-team-blocked:spending-limit` — accounts need a SuperGrok subscription or API credits. The OAuth tokens are valid (scope includes `api:access`) but the upstream rejects all requests. The gw46 gateway will show `credentials: N` in health but every API call fails with 402.
|
||||
|
||||
**Workarounds being investigated:**
|
||||
- Check if new accounts get free trial credits after email verification
|
||||
- Use Grok website API (grok.com/rest/) with SSO cookies instead of api.x.ai
|
||||
- Use a different provider entirely
|
||||
|
||||
### Automated SSO → OAuth Conversion (Working Method)
|
||||
|
||||
**Script**: `sso2oauth.py` in the GrokRegister directory.
|
||||
|
||||
**Prerequisites:**
|
||||
- DrissionPage (installed globally, import with `python -s` to avoid Hermes venv path issues)
|
||||
- httpx, sqlite3 (stdlib)
|
||||
- Chrome installed at `C:\Program Files\Google\Chrome\Application\chrome.exe`
|
||||
- gw46 gateway DB at `~/Documents/grok46gw/grok46_accounts.db`
|
||||
|
||||
**Flow:**
|
||||
1. Read SSO tokens from `keys/accounts.txt` (format: `email:password:sso`)
|
||||
2. Start OAuth device code flow: `POST https://auth.x.ai/oauth2/device/code`
|
||||
3. Launch DrissionPage with SSO cookie injected into `accounts.x.ai` domain
|
||||
4. Navigate to `verification_uri_complete` (includes `user_code` in URL)
|
||||
5. Click "Продолжить" (Continue) to submit the code
|
||||
6. Click "Authorize" / "Разрешить" to grant device authorization
|
||||
7. Poll for OAuth token: `POST https://auth.x.ai/oauth2/token` with `grant_type=device_code`
|
||||
8. Store token in gw46 SQLite DB via `INSERT INTO accounts`
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
cd ~/tmp/grok_register/grok-register
|
||||
python -s sso2oauth.py --concurrent 1 --count 10
|
||||
python -s sso2oauth.py --start 10 --count 50
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `--count N`: number of accounts to process (0 = all)
|
||||
- `--start N`: start index
|
||||
- `--concurrent N`: concurrency (DrissionPage crashes with >1, use always 1)
|
||||
|
||||
**DrissionPage Concurrency Limitation (CRITICAL):**
|
||||
DrissionPage 4.1.0 raises `PageDisconnectedError` (与页面的连接已断开) when multiple `ChromiumPage` instances are created in the same process — even with `asyncio.Semaphore`. The background event handler thread crashes. **Always use `--concurrent 1`** (sequential processing). For large batches, split into chunks and restart the Python process between chunks to clear DrissionPage's internal state (~10-15 accounts per process before memory leaks cause crashes).
|
||||
|
||||
**Performance:**
|
||||
- ~10-15 seconds per account (sequential)
|
||||
- ~74 accounts processed in ~12 minutes
|
||||
|
||||
### gw46 Gateway KeyPool Refresh
|
||||
|
||||
After adding tokens to the SQLite DB externally (via `sso2oauth.py`), the gw46 gateway's **in-memory KeyPool is NOT automatically refreshed**. The gateway only reads the DB during startup (`lifespan` event) and when `/oauth/callback` or `/accounts/api-key` endpoints are called.
|
||||
|
||||
**To refresh after external DB writes:**
|
||||
1. Kill the gateway: `taskkill /F /PID <PID>`
|
||||
2. Restart: `cd ~/Documents/grok46gw && python gw46.py`
|
||||
3. Verify: `curl http://127.0.0.1:20146/health` — `credentials` count should match DB total
|
||||
|
||||
**gw46 Gateway Integration (Manual)**
|
||||
1. Start gw46: `python Documents/grok46gw/gw46.py` (port 20146)
|
||||
2. Get device code: `GET /oauth/device`
|
||||
3. Auth in browser with SSO cookie → authorize
|
||||
4. Poll for token: `GET /oauth/device/poll`
|
||||
5. Token stored in gw46 SQLite, used for API proxying
|
||||
|
||||
## Working Approach (v13 — Deprecated)
|
||||
|
||||
The custom Playwright scripts (v13, v16, v20) are **deprecated** in favor of the ready-made solutions above. They are kept for reference but the DrissionPage-based approach + gw46.py gateway is more reliable.
|
||||
```
|
||||
C:\Users\User\Desktop\авторег проект\grok_autoreg_v13.py
|
||||
```
|
||||
|
||||
**v11 is NOT usable** — the file at `_SCRIPTS/grok_autoreg_v11.py` has a syntax error (broken indentation at line 80-81, `for part in msg.walk():` is not indented after `if msg.is_multipart():`). Do NOT reference v11 as a working alternative unless the file has been repaired.
|
||||
|
||||
**Which to use**: Always use v13. v11 exists at `_SCRIPTS/` but is non-functional.
|
||||
|
||||
**Key insight**: Signup happens INSIDE the device-auth OAuth flow, NOT before it.
|
||||
|
||||
### Flow
|
||||
1. Start `grok login --device-auth` → get device URL
|
||||
2. Open device URL in browser → click Continue → redirected to sign-in
|
||||
3. Click "Sign up" (NOT "Login") on the sign-in page
|
||||
4. Click "Sign up with email" → fill email → submit
|
||||
5. Solve Turnstile (2Captcha) on sign-up page
|
||||
6. Get OTP from IMAP (t-online.de) → enter code via `type()` (not `fill()`)
|
||||
7. On verification page: solve Turnstile again → click submit
|
||||
8. On profile form: fill name + surname + password via `fill()` + dispatch `input` event → click "Завершить регистрацию"
|
||||
9. **CRITICAL**: Navigate BACK to the device auth URL → click Continue → click Authorize/Allow
|
||||
10. Token appears in `~/.grok/auth.json` → add to `token_pool.json`
|
||||
|
||||
### Device Auth Flow Is NOT Automatic
|
||||
After signup completes, the browser is NOT automatically redirected to the auth callback. You MUST:
|
||||
1. Navigate back to the device auth URL
|
||||
2. Click "Continue" (user is now logged in, so it shows the authorization page)
|
||||
3. Click "Authorize" or "Allow"
|
||||
4. ONLY THEN does the device auth CLI receive the callback and write the token
|
||||
|
||||
Without step 9, the account is created but the token is NEVER saved — the device auth CLI times out waiting for a callback that never arrives.
|
||||
|
||||
### Dependencies
|
||||
- **patchright** (NOT playwright): `pip install patchright`
|
||||
- **requests**: `pip install requests` (used by 2Captcha solver)
|
||||
- **2Captcha** key: `[REDACTED-KEY]` (the `bd9c67...` key is DEAD)
|
||||
- **IMAP**: t-online.de via `secureimap.t-online.de:993`
|
||||
- **Browser**: Uses `launch_persistent_context` with `channel="chrome"` and `user_data_dir=r"C:\Users\User\Desktop\browser_profile_grok"`
|
||||
- **Emails file**: `C:\Users\User\Downloads\Telegram Desktop\working_mails.txt` (format: `email:password`)
|
||||
|
||||
### Usage
|
||||
```bash
|
||||
cd "C:\Users\User\Desktop\авторег проект"
|
||||
PYTHONPATH="" .venv/Scripts/python -u "C:/Users/User/projects/_uncategorized_projects/_SCRIPTS/grok_autoreg_v11.py" 100
|
||||
```
|
||||
Argument = number of accounts to register.
|
||||
|
||||
## CRITICAL PITFALL: Do Not Fix What Is Not Broken
|
||||
|
||||
When Vlad says the software "worked before" and it is failing now:
|
||||
1. **DO NOT modify the script** — search the ENTIRE PC for working versions first
|
||||
2. Use `search_files` with `*grok*autoreg*` across `C:\Users\User`
|
||||
3. The `Desktop\авторег проект\grok_autoreg.py` is NOT the working version — it is a different (broken) approach
|
||||
4. Multiple versions exist: `_SCRIPTS/grok_autoreg_v4.py` through `v11.py`
|
||||
|
||||
## Environment Issues
|
||||
|
||||
### PYTHONPATH Pollution
|
||||
The global PYTHONPATH points to `hermes-agent-0.18.2-latest\\venv` which has broken permissions. Always prefix with `PYTHONPATH=""` when running from a local venv.
|
||||
|
||||
### Python -s Flag for DrissionPage
|
||||
The Hermes venv (`hermes-agent-0.18.2-latest/venv/`) has broken file permissions on Playwright packages. When running DrissionPage from the global Python, use `python -s` to skip user site-packages — this avoids the permission error on `playwright/__init__.py`. Without `-s`, DrissionPage imports fail with `PermissionError: [Errno 13]` because Python scans the Hermes venv first.
|
||||
|
||||
```bash
|
||||
# Correct way to run DrissionPage scripts globally
|
||||
python -s sso2oauth.py --count 10
|
||||
```
|
||||
|
||||
### Venv Setup
|
||||
```bash
|
||||
cd "C:\Users\User\Desktop\авторег проект"
|
||||
python -m venv .venv
|
||||
PYTHONPATH="" .venv/Scripts/pip install patchright
|
||||
```
|
||||
|
||||
## Token Pool
|
||||
- Location: `~/.grok/token_pool.json`
|
||||
- Format: `{"tokens": [{"email": ..., "access_token": ..., "refresh_token": ..., "expires_at": ...}]}`
|
||||
- Tokens expire after ~6 hours, auto-refresh via `TokenPool.get_valid()`
|
||||
|
||||
## Fixes Applied to v11 (2026-08-13)
|
||||
|
||||
### IMAP: UNSEEN Filter + Socket Timeout
|
||||
The original `mail.search(None, "ALL")` hangs on large mailboxes (5000+ emails). Fixed:
|
||||
1. Use `mail.search(None, '(UNSEEN FROM "x.ai")')` with fallback to `mail.search(None, "UNSEEN")` — searches only new emails, not entire mailbox
|
||||
2. Add `mail = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT, timeout=30)` and `mail.socket().settimeout(30)` to prevent hanging on slow connections
|
||||
3. Wrap each search in a try/except to catch socket timeouts
|
||||
|
||||
### Code Entry: type() Instead of fill()
|
||||
`code_input.fill(vcode)` does not trigger form validation events. Fixed to use `code_input.type(code_clean, delay=80)` with dash removed (`vcode.replace("-", "")`).
|
||||
|
||||
### Button Selectors: Exclude OneTrust Cookie Buttons
|
||||
`button:has-text('Подтвердить')` matches the OneTrust cookie consent button `onetrust-close-btn-handler`. Fixed by checking `class` attribute for `"onetrust"` and skipping those buttons.
|
||||
|
||||
### JS `querySelector` with `:has-text()` Is Invalid
|
||||
`document.querySelector('button:has-text("...")')` throws `SyntaxError` because `:has-text()` is a Playwright pseudo-selector, NOT a valid CSS selector. Use only `page.locator("button:has-text('...')")` in Playwright context. For JS fallback, use loop with `textContent.includes()`:
|
||||
|
||||
### Missing "Завершить регистрацию" Button
|
||||
The profile completion form uses "Завершить регистрацию" (Complete registration), not "Sign up" or "Далее". Added to the button text list in Step 11.
|
||||
|
||||
### Profile Form: `fill()` + `dispatchEvent('input')` for React
|
||||
The profile form (name, surname, password) uses React controlled inputs. Using `Object.getOwnPropertyDescriptor(...).set` bypasses React's change detection — the form sees empty values and refuses to submit. Fix:
|
||||
1. Use `locator.fill()` for all three fields
|
||||
2. After each `fill()`, dispatch an `input` event via `page.evaluate("document.querySelector('input[...]')?.dispatchEvent(new Event('input', {bubbles: true}))")`
|
||||
3. Then click the submit button via Playwright locator (NOT JS `querySelector` with `:has-text()` — that's a Playwright pseudo-selector, not valid CSS)
|
||||
4. Use Playwright locator: `page.locator("button:has-text('Завершить регистрацию')").first`
|
||||
|
||||
### Turnstile on Verification Page
|
||||
The verification code page also has a Turnstile captcha. Added detection and solving in Step 10 (before clicking submit).
|
||||
|
||||
### Rate-Limiting Avoidance
|
||||
x.ai throttles after 1-2 repeated attempts on the same email. The script skips rate-limited emails automatically. Do NOT re-run the same email repeatedly — move to a fresh one.
|
||||
|
||||
## Other Versions (for reference)
|
||||
- `v4`–`v10`: Various iterations in `_SCRIPTS/`
|
||||
- `grok_cdp_autoreg.py`: CDP-based approach (may work differently)
|
||||
- `_PROJECTS/grok_autoreg.py`: Another project variant
|
||||
- `Desktop\авторег проект\grok_autoreg.py`: The BROKEN version (signup-then-auth, does not work)
|
||||
|
||||
## v16: OAuth-After-Signup + Persistent Context (2026-08-13)
|
||||
|
||||
v16 is the current working approach. Key improvements over v11-v15:
|
||||
|
||||
### Persistent Context with Temp User Data Dir
|
||||
- Uses `launch_persistent_context(user_data_dir=temp_dir, channel="chrome")` — NOT `launch()`
|
||||
- Temp directory per registration (no session leakage across accounts)
|
||||
- `--disable-blink-features=AutomationControlled` is REQUIRED for Turnstile to work
|
||||
- **Without `channel="chrome"`**: form submission consistently fails (GET params instead of POST redirect)
|
||||
- **With `channel="chrome"`**: form submission works ~50% of the time (Turnstile-dependent)
|
||||
|
||||
### Form Submission Pattern (v16 Working)
|
||||
The profile page form submission is flaky. The pattern that works:
|
||||
1. Solve Turnstile on profile page (sitekey `0x4AAAAAAAhr9JGVDZbrZOo0`)
|
||||
2. Inject token via `querySelectorAll('input[name="cf-turnstile-response"]')` + string interpolation
|
||||
3. Click "Завершить регистрацию" button via Playwright locator
|
||||
4. Wait 3s, then force `form.submit()` via JS on the form containing password input
|
||||
5. Wait 5s for redirect
|
||||
6. If still on sign-up (GET params in URL), the account may still have been created — proceed to OAuth
|
||||
|
||||
### OAuth Timing Fix
|
||||
**CRITICAL**: `grok login --oauth` times out after ~2 minutes. Signup takes 3-5 minutes (IMAP + Turnstile).
|
||||
- **BEFORE v16**: OAuth was started before signup → CLI times out → callback received but no token saved
|
||||
- **v16 FIX**: Device auth for signup, OAuth started AFTER signup completes → CLI is fresh, no timeout
|
||||
|
||||
### Flow (v16)
|
||||
1. Clear auth.json (backup to .bak, restore if empty)
|
||||
2. `grok login --device-auth` → get device URL
|
||||
3. Browser: device URL → Continue → Sign up → Sign up with email
|
||||
4. Fill email → submit → IMAP code → enter code
|
||||
5. Turnstile on code page (often unsolvable, page advances anyway)
|
||||
6. Profile form: fill name/surname/password → Turnstile on profile → solve
|
||||
7. Click "Завершить регистрацию" → force form.submit()
|
||||
8. If redirected to consent page: click "Authorize" → device auth CLI gets token
|
||||
9. If device auth fails: `grok login --oauth` (fresh) → navigate to OAuth URL → consent page → Authorize
|
||||
10. Extract token from auth.json (newest entry, sorted by key)
|
||||
|
||||
### Success Rate & Token Validation
|
||||
- Form submission redirects to consent page ~50% of runs
|
||||
- When it does: token is valid for the NEW account
|
||||
- When it doesn't: OAuth fallback, but account may not have been created → no token
|
||||
- **Always validate tokens**: `curl https://api.x.ai/v1/models -H "Authorization: Bearer TOKEN"`
|
||||
- Pool tokens may show "valid" (based on expiry time) but fail API calls due to wrong account binding
|
||||
|
||||
### Current Working Script
|
||||
```
|
||||
C:\Users\User\Desktop\авторег проект\grok_autoreg_v16.py
|
||||
```
|
||||
Commands: `register`, `batch N`, `pool`, `proxy [port]`, `health`, `test`
|
||||
|
||||
### Lock File & Stale Process Cleanup
|
||||
|
||||
Before running ANY grok login command, cleanup stale state:
|
||||
|
||||
```python
|
||||
import subprocess, os
|
||||
|
||||
# Kill stale grok processes
|
||||
subprocess.run(["taskkill", "/f", "/im", "grok.exe"], capture_output=True, timeout=5)
|
||||
|
||||
# Remove lock files that block fresh auth
|
||||
for f in [AUTH_JSON + ".lock", os.path.join(GROK_HOME, "leader.sock")]:
|
||||
if os.path.exists(f):
|
||||
os.remove(f)
|
||||
```
|
||||
|
||||
Without this, `grok logout` and `grok login --device-auth` hang indefinitely with `TimeoutError`.
|
||||
|
||||
### Turnstile Timeout Parameter
|
||||
|
||||
The `solve_turnstile()` function now supports a `timeout` parameter (seconds, default 30). The code-page Turnstile (sitekey `0x4AAAAAAADnQ4F4U8W2nQyH`) is ALWAYS unsolvable via 2captcha — do not wait 5 minutes for it:
|
||||
|
||||
```python
|
||||
token = await solve_turnstile(sk, page.url, timeout=30)
|
||||
if token:
|
||||
# inject
|
||||
else:
|
||||
print("unsolvable/timeout — continuing anyway")
|
||||
```
|
||||
|
||||
The profile-page Turnstile (sitekey `0x4AAAAAAAhr9JGVDZbrZOo0`) IS solvable (~30s). Use the same timeout.
|
||||
|
||||
### React SPA: No HTML Forms (v19-v20 Finding)
|
||||
|
||||
The x.ai signup page (`/sign-up`) is a React SPA that does NOT contain `<form>` elements. The signup is done through JavaScript API calls (likely Zitadel API). This means:
|
||||
|
||||
- `form.submit()` from JS does NOTHING useful (navigates to GET params URL)
|
||||
- `page.keyboard.press("Enter")` also does NOTHING (no form to submit)
|
||||
- The button click triggers a React event handler that makes an API call
|
||||
- Even with valid Turnstile tokens injected into `cf-turnstile-response` hidden inputs, the React event handler may not check that input
|
||||
|
||||
**As of v20, the form submission is the unsolved bottleneck.** The Turnstile is solved, the token is injected, but the server rejects the submission ~90% of the time. Flow degrades to:
|
||||
|
||||
1. Signup form stuck → account NOT created
|
||||
2. Device auth CLI waiting → no callback → timeout
|
||||
3. OAuth fallback → logs into old account → invalid token (403)
|
||||
|
||||
**v20 discovery**: `cf_clearance` cookie auto-solves on real Chrome (`channel="chrome"`). The cookie can be extracted and used for direct API calls. However, ALL x.ai API endpoints (`/api/v2/authn/verification_code`, `/api/v2/users/human`, etc.) return HTML (Next.js page) instead of JSON — the API is not directly accessible behind the Next.js frontend, even with a valid `cf_clearance` cookie.
|
||||
|
||||
### Known Failure Mode: API Token Validation
|
||||
|
||||
Even when `token_pool.json` shows tokens as "valid" (based on `expires_at`), they may return 403 `{"code":"unauthenticated:bad-credentials","error":"The OAuth2 access token could not be validated."}`. This happens when the OAuth flow authorized the WRONG account (existing account instead of newly created one). Always validate with:
|
||||
|
||||
```bash
|
||||
curl -s https://api.x.ai/v1/models -H "Authorization: Bearer TOKEN" | head -c 200
|
||||
```
|
||||
|
||||
### channel="chrome" Is Required
|
||||
|
||||
Without `channel="chrome"` in `launch_persistent_context`, the headless Chromium browser's Turnstile widget always shows a challenge (never auto-solves). With `channel="chrome"` (real Chrome), the widget auto-solves and sets `cf_clearance` cookie — no 2captcha needed for the WAF bypass. The `cf_clearance` cookie (`cf_clearance = MeLLMwuSWSia66h6...`) is set automatically after page load on real Chrome. However, the Turnstile widget on the profile form STILL needs manual solving (2captcha) because it's a separate challenge.
|
||||
|
||||
Always use:
|
||||
|
||||
```python
|
||||
browser = await pwr.chromium.launch_persistent_context(
|
||||
headless=False,
|
||||
channel="chrome", # REQUIRED — Turnstile auto-solves WAF with real Chrome
|
||||
args=["--window-size=1200,900", "--disable-blink-features=AutomationControlled", "--no-sandbox"]
|
||||
)
|
||||
```
|
||||
|
||||
## Reference Files
|
||||
- references/imap-pattern.md — IMAP verification code extraction with UNSEEN filter
|
||||
- references/react-form-filling.md — React controlled form filling with fill() + input event
|
||||
- references/device-auth-completion.md — Device auth flow completion after signup
|
||||
- references/oauth-fallback-token.md — OAuth fallback pattern for token retrieval
|
||||
- references/oauth-account-mismatch.md — OAuth authorizes wrong account (token bound to old account)
|
||||
- references/oauth-timing-account-binding.md — OAuth CLI timeout + account binding fix (v16)
|
||||
- references/turnstile-json-escape.md — Turnstile token JSON escaping
|
||||
- references/form-submission-pattern.md — Profile form submission pattern (v16 working)
|
||||
- references/profile-turnstile-timing.md — Profile Turnstile iframe detection timing fix
|
||||
- references/duckmail-api.md — DuckMail API reference (no auth, 19 domains, no rate limit)
|
||||
- references/curl-cffi-grpc-web.md — curl_cffi + gRPC-web registration approach (PRIMARY working method)
|
||||
- references/playwright-stealth-setup.md — Playwright stealth import pattern + Cloudflare bypass (2026-08-14)
|
||||
- references/bpproxy-pool.md — bpproxy residential proxy pool location + usage (2026-08-14)
|
||||
- references/tonline-email-pool.md — t-online.de email pool (17,893 accounts) + IMAP settings (2026-08-14)
|
||||
42
skills/grok-autoreg/references/bpproxy-pool.md
Normal file
42
skills/grok-autoreg/references/bpproxy-pool.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# bpproxy Residential Proxy Pool
|
||||
|
||||
## Location
|
||||
`C:/Users/User/tmp/bpproxy_hardsession_pool.txt`
|
||||
|
||||
## Format
|
||||
```
|
||||
# Account: <account_id>
|
||||
http://bpuser-<user>:<password>_hardsession-<session>@residential-x.bpproxy.at:1000
|
||||
http://bpuser-<user>:<password>_hardsession-<session>@datacenter-x.bpproxy.at:2000
|
||||
```
|
||||
|
||||
## Usage
|
||||
- **Residential proxies** (port 1000): Use these for Cloudflare-bypass. They rotate IPs and look like real residential connections.
|
||||
- **Datacenter proxies** (port 2000): Do NOT use for Cloudflare-protected sites. They get blocked.
|
||||
|
||||
## Filtering
|
||||
```python
|
||||
def load_proxies():
|
||||
proxies = []
|
||||
with open("C:/Users/User/tmp/bpproxy_hardsession_pool.txt", "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("http://") and "residential" in line:
|
||||
proxies.append(line)
|
||||
return proxies
|
||||
```
|
||||
|
||||
## Playwright Usage
|
||||
```python
|
||||
browser = await p.chromium.launch(
|
||||
headless=True,
|
||||
proxy={"server": proxy_url},
|
||||
args=["--no-sandbox"]
|
||||
)
|
||||
```
|
||||
|
||||
## ZTE Proxy (DO NOT USE for Playwright)
|
||||
- The ZTE HTTP forward proxy at `127.0.0.1:8888` (`C:/Users/User/Desktop/gh-autoreg/zte_proxy.py`) routes through ZTE MF935 USB modem
|
||||
- It works for HTTP requests (curl) but fails for Playwright's CONNECT tunnels
|
||||
- Playwright reports `net::ERR_PROXY_CONNECTION_FAILED`
|
||||
- Use bpproxy instead for Playwright-based automation
|
||||
72
skills/grok-autoreg/references/cf-clearance-bypass.md
Normal file
72
skills/grok-autoreg/references/cf-clearance-bypass.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# cf_clearance Cookie — Cloudflare WAF Bypass for x.ai
|
||||
|
||||
## Discovery (v20, 2026-08-13)
|
||||
|
||||
When using `channel="chrome"` (real Chrome via Playwright), the Cloudflare Turnstile challenge on `accounts.x.ai/sign-up` auto-solves WITHOUT 2captcha. The `cf_clearance` cookie is set automatically after page load.
|
||||
|
||||
**Cookie details:**
|
||||
```
|
||||
Name: cf_clearance
|
||||
Domain: .x.ai
|
||||
Value: MeLLMwuSWSia66h6YiouO4tSvXyrcfXDKMrqS4ifanM-178662...
|
||||
```
|
||||
|
||||
## How to Extract
|
||||
|
||||
```python
|
||||
# After page load
|
||||
cookies = await page.context.cookies()
|
||||
cf_clearance = ""
|
||||
for c in cookies:
|
||||
if c['name'] == 'cf_clearance':
|
||||
cf_clearance = c['value']
|
||||
break
|
||||
```
|
||||
|
||||
## Using for API Requests
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
cookies = {"cf_clearance": cf_clearance}
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Origin": "https://accounts.x.ai",
|
||||
"Referer": "https://accounts.x.ai/sign-up",
|
||||
}
|
||||
|
||||
resp = requests.post(
|
||||
"https://accounts.x.ai/api/v2/authn/verification_code",
|
||||
json={"email": "user@example.com"},
|
||||
headers=headers,
|
||||
cookies=cookies
|
||||
)
|
||||
```
|
||||
|
||||
## Limitation
|
||||
|
||||
Even with `cf_clearance`, ALL x.ai API endpoints return HTML (Next.js page) instead of JSON. The API is not directly accessible behind the Next.js frontend — the frontend catches all API routes and serves the React page.
|
||||
|
||||
**Tested endpoints (all return HTML with 200):**
|
||||
- `POST /api/v2/authn/verification_code`
|
||||
- `POST /api/v2/users/human`
|
||||
- `POST /api/v2/users`
|
||||
- `POST /api/v2/register`
|
||||
- `POST /api/v2/authn`
|
||||
- `POST /api/signup`
|
||||
- `POST /api/register`
|
||||
- `POST /api/v2/signup`
|
||||
- `POST /zitadel/v2/users/human`
|
||||
- `POST /api/v2beta/users/human`
|
||||
|
||||
**Without cf_clearance**, these endpoints return 403 (Cloudflare WAF).
|
||||
|
||||
## The Turnstile Widget Distinction
|
||||
|
||||
There are TWO Turnstile levels on x.ai:
|
||||
1. **Cloudflare WAF Turnstile** — auto-solved by real Chrome, sets `cf_clearance` cookie. This bypasses the WAF but doesn't help with the React form.
|
||||
2. **React form Turnstile** — the `cf-turnstile-response` widget on the profile form. This needs 2captcha. The token is injected into the hidden input, but the React event handler may not check it.
|
||||
|
||||
The `cf_clearance` solves #1 (WAF bypass) but #2 (form submission) remains unsolved.
|
||||
74
skills/grok-autoreg/references/curl-cffi-grpc-web.md
Normal file
74
skills/grok-autoreg/references/curl-cffi-grpc-web.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# GrokRegister: curl_cffi + gRPC-web Registration
|
||||
|
||||
**Location**: `C:\Users\User\tmp\grok_register\grok-register\grok.py`
|
||||
|
||||
## Architecture
|
||||
|
||||
The registration uses **curl_cffi** (TLS fingerprint emulation, not a real browser) and direct **gRPC-web** API calls to x.ai's Next.js backend.
|
||||
|
||||
## Key Components
|
||||
|
||||
### 1. Action ID Discovery
|
||||
```python
|
||||
html = session.get("https://accounts.x.ai/sign-up").text
|
||||
js_urls = [urljoin(start_url, m.group(0))
|
||||
for m in re.finditer(r"/_next/static/chunks/[^\"'\s>]+\.js", html)]
|
||||
for js_url in js_urls:
|
||||
js_content = session.get(js_url, timeout=15).text
|
||||
match = re.search(r'7f[a-fA-F0-9]{40}', js_content)
|
||||
if match:
|
||||
action_id = match.group(0)
|
||||
break
|
||||
```
|
||||
|
||||
### 2. gRPC-web OTP Sending
|
||||
```python
|
||||
def encode_grpc_message(field_id, string_value):
|
||||
key = (field_id << 3) | 2
|
||||
value_bytes = string_value.encode('utf-8')
|
||||
length = len(value_bytes)
|
||||
payload = struct.pack('B', key) + struct.pack('B', length) + value_bytes
|
||||
return b'\x00' + struct.pack('>I', len(payload)) + payload
|
||||
|
||||
session.post(f"{site_url}/auth_mgmt.AuthManagement/CreateEmailValidationCode",
|
||||
data=encode_grpc_message(1, email),
|
||||
headers={"content-type": "application/grpc-web+proto", "x-grpc-web": "1",
|
||||
"x-user-agent": "connect-es/2.1.1", "origin": site_url})
|
||||
```
|
||||
|
||||
### 3. Registration Submission
|
||||
```python
|
||||
headers = {
|
||||
"user-agent": user_agent, "accept": "text/x-component",
|
||||
"content-type": "text/plain;charset=UTF-8", "origin": site_url,
|
||||
"referer": f"{site_url}/sign-up",
|
||||
"next-router-state-tree": state_tree, "next-action": action_id,
|
||||
}
|
||||
payload = [{
|
||||
"emailValidationCode": verify_code,
|
||||
"createUserAndSessionRequest": {
|
||||
"email": email, "givenName": name, "familyName": surname,
|
||||
"clearTextPassword": password, "tosAcceptedVersion": "$undefined"
|
||||
},
|
||||
"turnstileToken": token, "promptOnDuplicateEmail": True
|
||||
}]
|
||||
res = session.post(f"{site_url}/sign-up", json=payload, headers=headers)
|
||||
```
|
||||
|
||||
### 4. SSO Cookie Extraction
|
||||
```python
|
||||
match = re.search(r'(https://[^"\s]+set-cookie\?q=[^:"\s]+)1:', res.text)
|
||||
if match:
|
||||
verify_url = match.group(1)
|
||||
session.get(verify_url, allow_redirects=True)
|
||||
sso = session.cookies.get("sso")
|
||||
```
|
||||
|
||||
## Key Insights
|
||||
- gRPC-web endpoint: `{site_url}/auth_mgmt.AuthManagement/CreateEmailValidationCode`
|
||||
- Content-type: `application/grpc-web+proto` (not JSON)
|
||||
- `x-grpc-web: 1` header required
|
||||
- `next-action` = action_id from JS chunks
|
||||
- `next-router-state-tree` = state tree from HTML
|
||||
- Turnstile sitekey: `0x4AAAAAAAhr9JGVDZbrZOo0`
|
||||
- SSO from `set-cookie?q=` redirect URL in response
|
||||
44
skills/grok-autoreg/references/device-auth-completion.md
Normal file
44
skills/grok-autoreg/references/device-auth-completion.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Device Auth Flow Completion
|
||||
|
||||
## Critical Pattern
|
||||
After signup is complete, the device auth CLI does NOT automatically receive the token. The browser must be explicitly navigated back to the device auth URL to complete the authorization.
|
||||
|
||||
## Flow
|
||||
1. Signup completes (profile form submitted)
|
||||
2. **Navigate back to device auth URL**: `await page.goto(url, ...)`
|
||||
3. **Click "Continue"**: User is now logged in, so the sign-in page shows the authorization card
|
||||
4. **Click "Authorize"** or "Allow": This redirects to the localhost callback URL
|
||||
5. Device auth CLI receives the callback and writes the token to `auth.json`
|
||||
|
||||
## Working Code
|
||||
```python
|
||||
# After signup (profile form submitted)
|
||||
print(f"Going back to device auth URL: {url}")
|
||||
await page.goto(url, timeout=30000, wait_until="domcontentloaded")
|
||||
await page.wait_for_timeout(3000)
|
||||
|
||||
# Click Continue (should show authorization page now)
|
||||
btn = page.locator("button:has-text('Продолжить')").first
|
||||
if await btn.count() == 0:
|
||||
btn = page.locator("button:has-text('Continue')").first
|
||||
if await btn.count() > 0:
|
||||
await btn.click()
|
||||
await page.wait_for_timeout(5000)
|
||||
|
||||
# Click Authorize
|
||||
auth_btn = page.locator("button:has-text('Authorize'), button:has-text('Allow')").first
|
||||
if await auth_btn.count() > 0:
|
||||
await auth_btn.click()
|
||||
await page.wait_for_timeout(5000)
|
||||
|
||||
# Now wait for token in auth.json
|
||||
for w in range(60):
|
||||
await asyncio.sleep(2)
|
||||
auth = load_auth()
|
||||
if f"https://auth.x.ai/oauth2/{email_addr}" in auth:
|
||||
# Token received!
|
||||
break
|
||||
```
|
||||
|
||||
## Common Failure
|
||||
Without this step, the account is created but the token is NEVER saved. The device auth CLI times out after 300 seconds waiting for a callback that never arrives because the browser never redirected to localhost.
|
||||
55
skills/grok-autoreg/references/duckmail-api.md
Normal file
55
skills/grok-autoreg/references/duckmail-api.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# DuckMail API Reference
|
||||
|
||||
**Base URL**: `https://api.duckmail.sbs`
|
||||
**Auth**: Most endpoints require no auth. Only `/messages` and `/messages/{id}` need a Bearer token.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET /domains
|
||||
Returns available email domains. No auth needed.
|
||||
```bash
|
||||
curl https://api.duckmail.sbs/domains
|
||||
```
|
||||
Returns: `{"hydra:member": [{"domain": "duckmail.sbs", ...}, ...], "hydra:totalItems": 19}`
|
||||
|
||||
### POST /accounts
|
||||
Create a new temporary email account. No auth needed.
|
||||
```bash
|
||||
curl -X POST https://api.duckmail.sbs/accounts \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"address": "user@duckmail.sbs", "password": "StrongPass123!"}'
|
||||
```
|
||||
Returns: `{"id": "...", "address": "...", "isActive": false, "expiresAt": "2026-08-14T..."}`
|
||||
|
||||
### POST /token
|
||||
Get JWT auth token for an account. No auth needed.
|
||||
```bash
|
||||
curl -X POST https://api.duckmail.sbs/token \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"address": "user@duckmail.sbs", "password": "StrongPass123!"}'
|
||||
```
|
||||
Returns: `{"id": "...", "token": "eyJhbG..."}`
|
||||
|
||||
### GET /messages
|
||||
List messages in the inbox. Requires Bearer token.
|
||||
```bash
|
||||
curl https://api.duckmail.sbs/messages \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
Returns: `{"hydra:member": [...], "hydra:totalItems": 0}`
|
||||
|
||||
### GET /messages/{id}
|
||||
Get a specific message. Requires Bearer token.
|
||||
```bash
|
||||
curl https://api.duckmail.sbs/messages/{id} \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
Returns: Full message with subject, intro, text, html fields.
|
||||
|
||||
## Notes
|
||||
- mail.tm-compatible API — code written for mail.tm works with s/base URL change
|
||||
- No bearer token needed for account creation or token retrieval
|
||||
- No rate limiting observed (tested with 170+ accounts in ~7 minutes)
|
||||
- 19 domains available, rotates automatically
|
||||
- Emails expire after ~24 hours
|
||||
- `isActive: false` on creation — accounts are created inactive but still receive email
|
||||
66
skills/grok-autoreg/references/form-submission-pattern.md
Normal file
66
skills/grok-autoreg/references/form-submission-pattern.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Form Submission Pattern (v16 Working)
|
||||
|
||||
## The Problem
|
||||
The profile page "Завершить регистрацию" (Complete sign up) form submission is unreliable. After solving the Turnstile and clicking the button, the form sometimes:
|
||||
- Redirects to the consent page (success — ~50% of runs)
|
||||
- Stays on sign-up page with GET params appended (failure — form submits as GET instead of POST)
|
||||
- Stays on sign-up page with no URL change (button click doesn't trigger form)
|
||||
|
||||
## Root Cause
|
||||
The Turnstile token injection works, but the form submission behavior depends on whether the Cloudflare Turnstile server-side validation accepts the token. When the token is valid:
|
||||
- Button click → POST redirect → consent page
|
||||
When the token is invalid:
|
||||
- Button click → GET params → same page (server rejects the form)
|
||||
|
||||
The sitekey is `0x4AAAAAAAhr9JGVDZbrZOo0` (fallback; actual sitekey extracted from iframe `src` attribute).
|
||||
|
||||
## Working Pattern (dani.thaler99 run)
|
||||
```
|
||||
[S10] Turnstile solved!
|
||||
[S11] Clicked 'Завершить регистрацию' -> https://accounts.x.ai/account
|
||||
[S11] After submit: https://accounts.x.ai/oauth2/consent?...
|
||||
[S12] Redirected: https://accounts.x.ai/oauth2/consent?...
|
||||
```
|
||||
|
||||
The button click redirected to `/account`, then `form.submit()` redirected to `/oauth2/consent`. This was with Chromium (not Chrome), `--disable-blink-features=AutomationControlled`, and `--no-sandbox`.
|
||||
|
||||
## Working Pattern (p261250 run, v15)
|
||||
```
|
||||
[10] Turnstile solved!
|
||||
[10] Clicked 'Завершить регистрацию' -> https://accounts.x.ai/oauth2/device?user_code=TGQT-FDR8
|
||||
[11] Redirected to: https://accounts.x.ai/oauth2/device?user_code=TGQT-FDR8
|
||||
```
|
||||
|
||||
This was with `channel="chrome"` (system Chrome), `launch_persistent_context` with temp user_data_dir.
|
||||
|
||||
## Non-Working Pattern (bobozkurt, christian.klausch87, felixschade)
|
||||
```
|
||||
[S10] Turnstile solved!
|
||||
[S11] Clicked 'Завершить регистрацию' -> https://accounts.x.ai/sign-up
|
||||
[S11] After submit: https://accounts.x.ai/sign-up?email=...&givenName=Max&familyName=Mustermann
|
||||
```
|
||||
|
||||
The form submits as GET with query params. The account may or may not be created. The OAuth fallback navigates to the OAuth URL but the sign-in page is shown (account not created).
|
||||
|
||||
## Code Pattern
|
||||
```python
|
||||
# Click button
|
||||
btn = page.locator(f"button:has-text('Завершить регистрацию'):not([class*='onetrust'])").first
|
||||
if await btn.count() > 0:
|
||||
await btn.click()
|
||||
await page.wait_for_timeout(3000)
|
||||
# Force form submission
|
||||
await page.evaluate("""
|
||||
const forms = document.querySelectorAll('form');
|
||||
for (const f of forms) {
|
||||
if (f.querySelector('input[type="password"]')) {
|
||||
f.submit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
""")
|
||||
await page.wait_for_timeout(5000)
|
||||
```
|
||||
|
||||
## Key Insight
|
||||
Even when the form doesn't redirect, the account MIGHT still be created. The OAuth fallback (navigating to the OAuth URL after signup) can still work if the browser is logged in. But if the sign-in page is shown, the account was NOT created.
|
||||
87
skills/grok-autoreg/references/gw46-gateway.md
Normal file
87
skills/grok-autoreg/references/gw46-gateway.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Grok46 Gateway (gw46.py)
|
||||
|
||||
## Location
|
||||
`C:\Users\User\Documents\grok46gw\gw46.py`
|
||||
|
||||
## Overview
|
||||
A ready-made OpenAI-compatible API gateway for xAI's Grok models with OAuth auto-registration, device code flow, and SQLite-based account management.
|
||||
|
||||
## Features
|
||||
- **OpenAI-compatible API**: `/v1/chat/completions`, `/v1/models`, `/v1/responses`
|
||||
- **Multi-model support**: grok-4.6, grok-4.5, grok-4.3, grok-build-0.1
|
||||
- **OAuth PKCE flow**: `/oauth/login` → user signs in browser → callback → token
|
||||
- **Device code flow**: `/oauth/device` → user_code + verification_uri → `/oauth/device/poll`
|
||||
- **SQLite database**: accounts stored in `grok46_accounts.db` with refresh tokens
|
||||
- **API key pool**: round-robin rotation across all stored credentials
|
||||
- **Token refresh**: automatic refresh for expired tokens (if refresh_token is available)
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Start the gateway
|
||||
cd ~/Documents/grok46gw
|
||||
python gw46.py
|
||||
# Listens on 127.0.0.1:20146 by default (configurable via GROK46_PORT)
|
||||
```
|
||||
|
||||
## Getting Tokens via Device Code Flow
|
||||
|
||||
1. Start the gateway: `python gw46.py`
|
||||
2. Request a device code: `GET http://127.0.0.1:20146/oauth/device`
|
||||
3. Response includes `user_code` (e.g. `J2QB-MGQC`) and `verification_uri_complete` (e.g. `https://accounts.x.ai/oauth2/device?user_code=J2QB-MGQC`)
|
||||
4. Open the verification URI in a browser
|
||||
5. Sign in with an existing x.ai account (or create a new one)
|
||||
6. Enter the user_code and authorize the application
|
||||
7. Poll for the token: `GET http://127.0.0.1:20146/oauth/device/poll`
|
||||
8. When authorized, the response includes the `access_token` and the token is saved to the database
|
||||
|
||||
## Account Management
|
||||
|
||||
- `GET /accounts` — list all stored accounts and API keys
|
||||
- `POST /accounts/api-key?key=...&label=...` — add a manual API key
|
||||
- `GET /health` — health check with credential count
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `GROK46_PORT` — port (default: 20146)
|
||||
- `GROK46_HOST` — bind host (default: 127.0.0.1)
|
||||
- `GROK46_DB` — SQLite DB path (default: `./grok46_accounts.db`)
|
||||
- `XAI_API_KEYS` — comma-separated xAI API keys to add to the pool
|
||||
|
||||
## Database Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE accounts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT UNIQUE,
|
||||
access_token TEXT NOT NULL,
|
||||
refresh_token TEXT,
|
||||
expires_at REAL,
|
||||
scope TEXT,
|
||||
created_at REAL DEFAULT (strftime('%s','now')),
|
||||
last_used_at REAL,
|
||||
use_count INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE api_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT UNIQUE NOT NULL,
|
||||
label TEXT,
|
||||
created_at REAL DEFAULT (strftime('%s','now')),
|
||||
last_used_at REAL,
|
||||
use_count INTEGER DEFAULT 0
|
||||
);
|
||||
```
|
||||
|
||||
## OAuth Auto-Registration Flow
|
||||
|
||||
The gateway implements the full PKCE OAuth flow:
|
||||
1. `GET /oauth/login` — redirects to x.ai OAuth authorization page with PKCE challenge
|
||||
2. User signs in and authorizes in the browser
|
||||
3. Browser redirects to `http://127.0.0.1:20146/oauth/callback?code=...`
|
||||
4. Gateway exchanges the code for tokens using the PKCE verifier
|
||||
5. Tokens are saved to the SQLite database and added to the credential pool
|
||||
|
||||
## Token Refresh
|
||||
|
||||
Tokens with `refresh_token` are automatically refreshed when they expire. The gateway calls the xAI token endpoint with `grant_type=refresh_token` to get new access tokens.
|
||||
36
skills/grok-autoreg/references/imap-pattern.md
Normal file
36
skills/grok-autoreg/references/imap-pattern.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# IMAP Verification Code Extraction Pattern
|
||||
|
||||
## Key Lessons
|
||||
- `mail.search(None, "ALL")` HANGS on large mailboxes (5000+ emails)
|
||||
- **Fix**: Use `mail.search(None, '(UNSEEN FROM "x.ai")')` with fallback to `mail.search(None, "UNSEEN")`
|
||||
- **Socket timeout**: Always set `mail = imaplib.IMAP4_SSL(host, port, timeout=30)` and `mail.socket().settimeout(30)`
|
||||
- Wrap each search in try/except
|
||||
|
||||
## Working Code Snippet
|
||||
```python
|
||||
def get_verification_code(email_addr, password, timeout=180):
|
||||
import socket
|
||||
mail = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT, timeout=30)
|
||||
mail.socket().settimeout(30)
|
||||
mail.login(email_addr, password)
|
||||
mail.select("INBOX")
|
||||
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
try:
|
||||
typ, data = mail.search(None, '(UNSEEN FROM "x.ai")')
|
||||
if typ != "OK":
|
||||
typ, data = mail.search(None, "UNSEEN")
|
||||
if typ == "OK" and data[0]:
|
||||
ids = data[0].split()
|
||||
for mid in reversed(ids):
|
||||
# fetch and parse message
|
||||
...
|
||||
if "x.ai" in sender.lower():
|
||||
code_match = re.search(r'\b([A-Z0-9]{3}-[A-Z0-9]{3})\b', body)
|
||||
if code_match:
|
||||
return code_match.group(1)
|
||||
except Exception as e:
|
||||
print(f"IMAP search error: {e}")
|
||||
time.sleep(5)
|
||||
```
|
||||
34
skills/grok-autoreg/references/oauth-account-mismatch.md
Normal file
34
skills/grok-autoreg/references/oauth-account-mismatch.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# OAuth Token Account Mismatch
|
||||
|
||||
## Problem
|
||||
After signup via device auth flow, the OAuth fallback (grok login --oauth) retrieves a token that appears valid but fails API calls with `bad-credentials`.
|
||||
|
||||
## Root Cause
|
||||
The OAuth flow authorizes the WRONG account:
|
||||
1. The signup creates a new account in the browser
|
||||
2. grok logout clears the CLI's auth.json
|
||||
3. grok login --oauth starts a fresh OAuth session
|
||||
4. The browser navigates to the OAuth URL
|
||||
5. The browser's session is for the OLD account (from persistent profile), not the new one
|
||||
6. The OAuth authorizes the old account and saves the token with the old account's refresh_token
|
||||
7. The script stores the token with the new email but the OLD account's credentials
|
||||
|
||||
## Detection
|
||||
All tokens in the pool have the same refresh_token even though they have different emails.
|
||||
|
||||
Check with:
|
||||
```bash
|
||||
python -c "import json; p=json.load(open(r'C:\Users\User\.grok\token_pool.json')); [print(t.get('refresh_token','')[:20]) for t in p['tokens']]"
|
||||
```
|
||||
|
||||
If all tokens have the same refresh_token prefix, they are all bound to the same account.
|
||||
|
||||
## Fix
|
||||
Use v13's fresh browser context (browser.new_context() not launch_persistent_context). This ensures the browser is logged in to the NEW account after signup, so the OAuth fallback authorizes the correct account.
|
||||
|
||||
## Verification
|
||||
After registration, verify the token with a test API call:
|
||||
```bash
|
||||
curl -s http://localhost:8318/v1/chat/completions -X POST -H "Content-Type: application/json" -d '{"model":"grok-4.5","messages":[{"role":"user","content":"test"}],"max_tokens":5}'
|
||||
```
|
||||
If this returns `bad-credentials`, the token is bound to the wrong account.
|
||||
43
skills/grok-autoreg/references/oauth-fallback-token.md
Normal file
43
skills/grok-autoreg/references/oauth-fallback-token.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# OAuth Fallback Token Retrieval
|
||||
|
||||
After signup completes (profile form submitted), the browser is logged in but the device auth CLI may not have received the callback. The reliable fallback:
|
||||
|
||||
## Flow
|
||||
1. `subprocess.run([grok_exe, "logout"], capture_output=True, timeout=10)` — clear old auth
|
||||
2. `subprocess.Popen([grok_exe, "login", "--oauth"], ...)` — start OAuth flow
|
||||
3. Parse OAuth URL from CLI output: `re.search(r'(https://auth\.x\.ai/oauth2/authorize\?\S+)', line)`
|
||||
4. `page.goto(oauth_url)` — navigate browser to OAuth URL
|
||||
5. If browser is logged in → auto-redirects to `127.0.0.1:XXXXX/callback?...`
|
||||
6. If not logged in → try clicking "Continue" or "Allow" buttons
|
||||
7. `proc.communicate(timeout=30)` — wait for CLI to finish
|
||||
8. Check `auth.json` for any key with `access_token`
|
||||
|
||||
## Code
|
||||
```python
|
||||
subprocess.run([GROK_EXE, "logout"], capture_output=True, timeout=10)
|
||||
proc = subprocess.Popen([GROK_EXE, "login", "--oauth"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
|
||||
oauth_url = None
|
||||
for line in iter(proc.stdout.readline, ''):
|
||||
if not line and proc.poll() is not None: break
|
||||
m = re.search(r'(https://auth\.x\.ai/oauth2/authorize\?\S+)', line)
|
||||
if m: oauth_url = m.group(1); break
|
||||
|
||||
if oauth_url:
|
||||
await page.goto(oauth_url, timeout=15000)
|
||||
await page.wait_for_timeout(5000)
|
||||
for w in range(15):
|
||||
if "127.0.0.1" in page.url or "localhost" in page.url: break
|
||||
btn = page.locator("button:has-text('Continue'), button:has-text('Allow')").first
|
||||
if await btn.count() > 0: await btn.click()
|
||||
await page.wait_for_timeout(2000)
|
||||
proc.communicate(timeout=30)
|
||||
|
||||
auth = json.load(open(AUTH_JSON))
|
||||
for key in auth:
|
||||
tok = auth[key].get("access_token") or auth[key].get("key", "")
|
||||
if tok: return tok
|
||||
```
|
||||
|
||||
## Why It Works
|
||||
The OAuth flow is simpler than device auth: the CLI prints an OAuth URL, the browser opens it, and since the browser is already logged in, it auto-redirects to localhost with the authorization code. The CLI picks up the redirect and writes the token to `auth.json`.
|
||||
@@ -0,0 +1,39 @@
|
||||
# OAuth Timing & Account Binding Issue
|
||||
|
||||
## The Problem
|
||||
`grok login --oauth` starts a local HTTP server that listens for a callback. The CLI times out after ~2 minutes. If the signup flow takes 3-5 minutes (IMAP code retrieval + Turnstile solving), the CLI times out BEFORE the callback arrives.
|
||||
|
||||
## Symptom
|
||||
```
|
||||
[12] Callback!
|
||||
[12] ❌ No token in auth.json
|
||||
```
|
||||
|
||||
The callback was received by the browser (redirected to `http://127.0.0.1:PORT/callback`), but the CLI had already timed out and exited. The token is never saved to auth.json.
|
||||
|
||||
## Fix (v16)
|
||||
Start OAuth AFTER signup, not before:
|
||||
1. Use device auth (`grok login --device-auth`) for the signup flow
|
||||
2. After signup completes (form submitted, account created), start `grok login --oauth`
|
||||
3. Navigate browser to the OAuth URL
|
||||
4. Browser is already logged in → consent page is shown immediately
|
||||
5. Click "Authorize" → callback arrives within seconds
|
||||
6. CLI saves token to auth.json
|
||||
|
||||
## Account Binding
|
||||
Even with the timing fix, the OAuth flow authorizes the account that the BROWSER is logged into. If the signup created the account and the browser is logged in, the token is for the new account. If the signup failed (form didn't submit), the browser is NOT logged in, and the OAuth page shows the sign-in page.
|
||||
|
||||
## Token Validation
|
||||
Always validate tokens after extraction:
|
||||
```python
|
||||
import urllib.request, json
|
||||
req = urllib.request.Request('https://api.x.ai/v1/models',
|
||||
headers={'Authorization': f'Bearer {token}'})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as r:
|
||||
print(f"Valid: {r.status}")
|
||||
except Exception as e:
|
||||
print(f"Invalid: {e}")
|
||||
```
|
||||
|
||||
A 403 response means the token is bound to the wrong account (OAuth authorized the old account from the previous session).
|
||||
46
skills/grok-autoreg/references/playwright-stealth-setup.md
Normal file
46
skills/grok-autoreg/references/playwright-stealth-setup.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Playwright Stealth Setup for x.ai
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install playwright-stealth
|
||||
```
|
||||
|
||||
## Import Pattern (2026-08-14 verified)
|
||||
|
||||
```python
|
||||
from playwright_stealth import Stealth
|
||||
|
||||
# Correct usage:
|
||||
stealth = Stealth()
|
||||
await stealth.apply_stealth_async(page)
|
||||
|
||||
# WRONG - does NOT work:
|
||||
# from playwright_stealth import stealth_async # ImportError
|
||||
# from playwright_stealth import stealth # module, not callable
|
||||
# await stealth(page) # TypeError
|
||||
```
|
||||
|
||||
## Why Stealth Is Needed
|
||||
|
||||
Without stealth, Cloudflare detects the automated browser and blocks the Clerk JS library from loading. The page HTML loads fine, but `window.Clerk` is `undefined`. The form submission triggers a 403 error from Cloudflare.
|
||||
|
||||
With stealth applied, the Clerk JS loads successfully and form submission works (when combined with residential proxy).
|
||||
|
||||
## Stealth + Residential Proxy
|
||||
|
||||
Stealth alone may not be enough — Cloudflare also checks the IP. Always combine with:
|
||||
- bpproxy residential hardsession proxies (`C:/Users/User/tmp/bpproxy_hardsession_pool.txt`)
|
||||
- Filter for `residential` lines (port 1000), not `datacenter` (port 2000)
|
||||
|
||||
## Launch Args
|
||||
|
||||
```python
|
||||
browser = await p.chromium.launch(
|
||||
headless=headless,
|
||||
proxy={"server": proxy_url},
|
||||
args=["--no-sandbox"] # --disable-blink-features=AutomationControlled breaks stealth
|
||||
)
|
||||
```
|
||||
|
||||
Do NOT use `--disable-blink-features=AutomationControlled` with stealth — stealth handles this internally.
|
||||
36
skills/grok-autoreg/references/profile-turnstile-timing.md
Normal file
36
skills/grok-autoreg/references/profile-turnstile-timing.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Profile Turnstile Detection Timing
|
||||
|
||||
## Problem
|
||||
The v13 script checks for the profile Turnstile at line 406:
|
||||
```python
|
||||
turnstile2 = page.locator("iframe[src*='challenges.cloudflare.com']").first
|
||||
if await turnstile2.count() > 0:
|
||||
sitekey = await turnstile2.get_attribute("src")
|
||||
...
|
||||
```
|
||||
|
||||
But `count()` returns 0 if the Turnstile iframe hasn't loaded yet. The profile form renders BEFORE the Turnstile widget, so the script misses it and submits without solving the captcha.
|
||||
|
||||
## Fix
|
||||
Replace the `count()` + `get_attribute()` pattern with `wait_for_selector()` + explicit timeout:
|
||||
|
||||
```python
|
||||
try:
|
||||
turnstile_iframe = page.locator("iframe[src*='challenges.cloudflare.com']").first
|
||||
await turnstile_iframe.wait_for(state="attached", timeout=10000)
|
||||
# Extract sitekey from the fully loaded iframe
|
||||
src = await turnstile_iframe.get_attribute("src")
|
||||
sk = re.search(r'sitekey=([^&]+)', src or "")
|
||||
if sk:
|
||||
sitekey = sk.group(1)
|
||||
token = await solve_turnstile(sitekey, page.url, timeout=45)
|
||||
if token:
|
||||
await page.evaluate(f"""document.querySelector('input[name="cf-turnstile-response"]').value = {json.dumps(token)}""")
|
||||
except Exception:
|
||||
print(" Profile Turnstile: not found or timed out")
|
||||
```
|
||||
|
||||
## Sitekey
|
||||
Profile page Turnstile uses sitekey `0x4AAAAAAAhr9JGVDZbrZOo0` — this IS solvable via 2captcha (~30s avg).
|
||||
|
||||
Code page Turnstile uses sitekey `0x4AAAAAAADnQ4F4U8W2nQyH` — ALWAYS unsolvable via 2captcha. Do not wait more than 30s for it.
|
||||
46
skills/grok-autoreg/references/react-form-filling.md
Normal file
46
skills/grok-autoreg/references/react-form-filling.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# React Form Filling Pattern for Browser Automation
|
||||
|
||||
## Problem
|
||||
React controlled inputs ignore `Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set` — the form validation sees empty values and refuses to submit.
|
||||
|
||||
## Solution
|
||||
Use `locator.fill()` + dispatch `input` event after each fill.
|
||||
|
||||
## Working Code Snippet
|
||||
```python
|
||||
# Fill name
|
||||
name_input = page.locator("input[autocomplete='given-name']").first
|
||||
await name_input.click()
|
||||
await name_input.fill("Max")
|
||||
await page.evaluate("""
|
||||
document.querySelector('input[autocomplete="given-name"]')
|
||||
?.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
""")
|
||||
|
||||
# Fill surname
|
||||
surname_input = page.locator("input[autocomplete='family-name']").first
|
||||
await surname_input.click()
|
||||
await surname_input.fill("Mustermann")
|
||||
await page.evaluate("""
|
||||
document.querySelector('input[autocomplete="family-name"]')
|
||||
?.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
""")
|
||||
|
||||
# Fill password (use fill() NOT JS value setter!)
|
||||
pwd_input = page.locator("input[type='password']").first
|
||||
await pwd_input.click()
|
||||
await pwd_input.fill(password)
|
||||
await page.evaluate("""
|
||||
document.querySelector('input[type="password"]')
|
||||
?.dispatchEvent(new Event('input', {bubbles: true}));
|
||||
""")
|
||||
|
||||
# Click submit — use Playwright locator, NOT JS querySelector with :has-text()
|
||||
btn = page.locator("button:has-text('Завершить регистрацию')").first
|
||||
await btn.click()
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
- **`:has-text()` in JS**: `document.querySelector('button:has-text("...")')` throws SyntaxError. `:has-text()` is a Playwright pseudo-selector, not valid CSS.
|
||||
- **JS fallback**: Use `textContent.includes()` loop if JS click is needed
|
||||
- **OneTrust buttons**: `button:has-text('Подтвердить')` matches cookie consent buttons. Check `class` attribute for "onetrust" and skip.
|
||||
123
skills/grok-autoreg/references/sso-to-oauth-conversion.md
Normal file
123
skills/grok-autoreg/references/sso-to-oauth-conversion.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# SSO → OAuth Conversion via Device Auth Flow
|
||||
|
||||
## Overview
|
||||
|
||||
Converts SSO session cookies (from GrokRegister) to OAuth access tokens via x.ai's OAuth device code flow. Uses DrissionPage to automate the browser authorization step.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.11+ with `httpx`, `sqlite3`
|
||||
- DrissionPage (`pip install DrissionPage`) — run with `python -s` to avoid Hermes venv path collisions
|
||||
- Chrome installed at `C:\Program Files\Google\Chrome\Application\chrome.exe`
|
||||
- gw46 gateway DB at `~/Documents/grok46gw/grok46_accounts.db`
|
||||
|
||||
## Script Location
|
||||
|
||||
`C:\Users\User\tmp\grok_register\grok-register\sso2oauth.py`
|
||||
|
||||
## Flow Detail
|
||||
|
||||
### Step 1: Start Device Code Flow
|
||||
```
|
||||
POST https://auth.x.ai/oauth2/device/code
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
client_id=b1a00492-073a-47ea-816f-4c329264a828&
|
||||
scope=openid+profile+email+offline_access+grok-cli:access+api:access
|
||||
|
||||
Response:
|
||||
{
|
||||
"device_code": "...",
|
||||
"user_code": "ABCD-1234",
|
||||
"verification_uri": "https://accounts.x.ai/oauth2/device",
|
||||
"verification_uri_complete": "https://accounts.x.ai/oauth2/device?user_code=ABCD-1234",
|
||||
"interval": 5,
|
||||
"expires_in": 600
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Authorize Device with SSO (DrissionPage)
|
||||
```python
|
||||
from DrissionPage import ChromiumPage
|
||||
|
||||
page = ChromiumPage()
|
||||
page.get("https://accounts.x.ai/sign-in")
|
||||
page.run_js(f"""
|
||||
document.cookie = "sso={sso_token}; domain=.accounts.x.ai; path=/; Secure;";
|
||||
document.cookie = "sso={sso_token}; domain=.x.ai; path=/; Secure;";
|
||||
""")
|
||||
page.get(verification_uri_complete) # includes user_code in URL
|
||||
# Click "Продолжить" (Continue) to submit the code
|
||||
page.ele("tag:button@@text()=Продолжить").click()
|
||||
# Wait for authorization page
|
||||
# Click "Authorize" / "Разрешить"
|
||||
page.ele("tag:button@@text()=Разрешить").click()
|
||||
```
|
||||
|
||||
### Step 3: Poll for Token
|
||||
```
|
||||
POST https://auth.x.ai/oauth2/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=urn:ietf:params:oauth:grant-type:device_code&
|
||||
device_code={device_code}&
|
||||
client_id=b1a00492-073a-47ea-816f-4c329264a828
|
||||
|
||||
Response (200):
|
||||
{
|
||||
"access_token": "eyJ0eX...",
|
||||
"refresh_token": "m9Vzs...",
|
||||
"expires_in": 3600,
|
||||
"scope": "openid profile email offline_access grok-cli:access api:access",
|
||||
"id_token": "eyJ0eX..."
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4: Store in SQLite
|
||||
```sql
|
||||
INSERT INTO accounts (email, access_token, refresh_token, expires_at, scope)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
```
|
||||
|
||||
## Token Validation
|
||||
|
||||
The OAuth token JWT contains:
|
||||
- `iss: https://auth.x.ai`
|
||||
- `aud: b1a00492-073a-47ea-816f-4c329264a828` (correct client_id)
|
||||
- `scope: openid profile email offline_access grok-cli:access api:access`
|
||||
- `principal_type: User`
|
||||
- `exp: <timestamp>` (6 hours from issue)
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### 1. DrissionPage Concurrency (PageDisconnectedError)
|
||||
DrissionPage [REDACTED-IP]b14 crashes with `PageDisconnectedError` (与页面的连接已断开) when multiple `ChromiumPage` instances are created in the same process. The background CDP event handler thread raises an unhandled exception. Cause is a shared CDP connection state across instances.
|
||||
|
||||
**Fix:** Force sequential processing (`--concurrent 1`). Split large batches into chunks of 10-15 with process restarts between chunks.
|
||||
|
||||
### 2. No API Credits
|
||||
OAuth tokens are valid (authenticate correctly) but accounts have no API credits. All API calls return:
|
||||
```json
|
||||
{"code":"personal-team-blocked:spending-limit",
|
||||
"error":"You have run out of credits or need a Grok subscription."}
|
||||
```
|
||||
The gw46 gateway shows `credentials: N` in health but every request fails with 402.
|
||||
|
||||
### 3. gw46 KeyPool Refresh
|
||||
The gateway reads the DB only during startup. External DB writes (via `sso2oauth.py`) are invisible until restart:
|
||||
```bash
|
||||
taskkill /F /PID <PID>
|
||||
cd ~/Documents/grok46gw && python gw46.py
|
||||
```
|
||||
|
||||
## Session Performance (2026-08-13)
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Accounts created | 171 |
|
||||
| SSO → OAuth converted | 74 |
|
||||
| Sequential time per account | ~10-15s |
|
||||
| Total conversion time | ~12 min |
|
||||
| Batch size before crash | ~10-15 |
|
||||
| DrissionPage chunks | 5 (restart between) |
|
||||
| Valid API tokens | 0 (all 402) |
|
||||
45
skills/grok-autoreg/references/tonline-email-pool.md
Normal file
45
skills/grok-autoreg/references/tonline-email-pool.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# t-online.de Email Pool
|
||||
|
||||
## Location
|
||||
`C:/Users/User/Desktop/_PROJECTS/GPT-AUTOREG/emails.txt`
|
||||
|
||||
## Count
|
||||
17,893 t-online.de email accounts with IMAP passwords.
|
||||
|
||||
## Format
|
||||
```
|
||||
[REDACTED-EMAIL]:[REDACTED]
|
||||
```
|
||||
|
||||
Comments start with `#`. Separator lines with `---` are skipped.
|
||||
|
||||
## IMAP Settings
|
||||
- **Server**: `secureimap.t-online.de`
|
||||
- **Port**: `993`
|
||||
- **SSL**: Yes
|
||||
|
||||
## Code Extraction Pattern
|
||||
```python
|
||||
import imaplib, email, re
|
||||
|
||||
mail = imaplib.IMAP4_SSL("secureimap.t-online.de", 993)
|
||||
mail.login(addr, pw)
|
||||
mail.select("inbox")
|
||||
_, msgs = mail.search(None, "UNSEEN")
|
||||
if not msgs[0]:
|
||||
_, msgs = mail.search(None, "ALL")
|
||||
for num in msgs[0].split()[-5:]: # Check last 5 messages
|
||||
_, data = mail.fetch(num, "(RFC822)")
|
||||
for part in data:
|
||||
if not isinstance(part, tuple): continue
|
||||
msg = email.message_from_bytes(part[1])
|
||||
# Extract body...
|
||||
codes = re.findall(r"\b(\d{6,8})\b", body)
|
||||
if codes and codes[0] != "000000": # Filter false positives
|
||||
return codes[0]
|
||||
```
|
||||
|
||||
## Pitfalls
|
||||
- **False positive "000000"**: Some emails contain this string (timestamps, IDs). Filter it out.
|
||||
- **UNSEEN filter**: Use `UNSEEN` first, fall back to `ALL` (last 5 messages) to avoid scanning thousands of messages.
|
||||
- **Gmail accounts are dead**: The Gmail accounts from `C:/Users/User/Desktop/_PROJECTS/tusk-gateway/tusk_gmail_accounts.json` have expired 2FA app passwords. Do NOT use them for IMAP.
|
||||
38
skills/grok-autoreg/references/turnstile-json-escape.md
Normal file
38
skills/grok-autoreg/references/turnstile-json-escape.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Turnstile Token JSON Escaping
|
||||
|
||||
The Turnstile token returned by 2Captcha is a long opaque string like:
|
||||
```
|
||||
1.7iQyv0Qs12EGBU8PpuyVlKig_yZf_G8Q-Tcu8r0OY7mDbX0QjCk...
|
||||
```
|
||||
|
||||
This string can contain dots, underscores, dashes, and other characters that break JavaScript string literals when interpolated directly.
|
||||
|
||||
## ❌ Never Do This
|
||||
```python
|
||||
await page.evaluate(f"el.value = '{token}';")
|
||||
# Causes: SyntaxError: Invalid left-hand side in assignment
|
||||
# When token contains dots or special chars: el.value = 1.7iQyv0Qs... (invalid)
|
||||
```
|
||||
|
||||
## ✅ Always Do This
|
||||
```python
|
||||
import json
|
||||
await page.evaluate(f"const el = document.querySelector('input[name=\"cf-turnstile-response\"]'); if (el) el.value = {json.dumps(token)};")
|
||||
```
|
||||
|
||||
`json.dumps()` produces a properly escaped JavaScript string literal:
|
||||
- Strings with dots: `"1.7iQyv0Qs..."` (valid JS)
|
||||
- Strings with quotes: `"it's \"good\""` (properly escaped)
|
||||
- Null/None: `null` (valid JS)
|
||||
|
||||
## Full Injection Pattern
|
||||
```python
|
||||
token = await solve_turnstile(sitekey, pageurl)
|
||||
if token:
|
||||
[REDACTED] json
|
||||
await page.evaluate(f"const el = document.querySelector('input[name=\"cf-turnstile-response\"]'); if (el) el.value = {json.dumps(token)};")
|
||||
await page.wait_for_timeout(500)
|
||||
```
|
||||
|
||||
## Why 2Captcha Returns "unsolvable"
|
||||
The Turnstile on the verification page is often reported as "unsolvable" by 2Captcha. This is a known issue with Cloudflare Turnstile — the challenge is intentionally easy for humans but hard for automated solvers. The page still advances without the Turnstile being solved (the code submission itself triggers the next step). So treat "unsolvable" as a non-fatal warning, not an error.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Turnstile Shadow DOM Bypass
|
||||
|
||||
## Discovery
|
||||
|
||||
The Cloudflare Turnstile widget on `accounts.x.ai` uses **shadow DOM** to encapsulate the challenge iframe. The standard approach of injecting the token into `cf-turnstile-response` hidden inputs often fails because the React SPA reads the token from the widget's callback, not from the DOM.
|
||||
|
||||
## The Bypass
|
||||
|
||||
The DrissionPage-based approach (from ReinerBRO/grok-register) directly accesses the shadow root of the Turnstile widget container and clicks the challenge button inside the iframe. This triggers the Turnstile challenge, which is auto-solved because the browser's `MouseEvent.screenX/screenY` are patched to realistic values.
|
||||
|
||||
## Code Pattern
|
||||
|
||||
```python
|
||||
# Find the cf-turnstile-response input
|
||||
challengeSolution = page.ele("@name=cf-turnstile-response")
|
||||
# Get its parent (the widget container with shadow DOM)
|
||||
challengeWrapper = challengeSolution.parent()
|
||||
# Access the shadow root to get the iframe
|
||||
challengeIframe = challengeWrapper.shadow_root.ele("tag:iframe")
|
||||
# Inject JavaScript into the iframe to patch MouseEvent
|
||||
challengeIframe.run_js("""
|
||||
window.dtp = 1
|
||||
function getRandomInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
let screenX = getRandomInt(800, 1200);
|
||||
let screenY = getRandomInt(400, 600);
|
||||
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: screenX });
|
||||
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: screenY });
|
||||
""")
|
||||
# Access the iframe's body shadow root to find the challenge button
|
||||
challengeIframeBody = challengeIframe.ele("tag:body").shadow_root
|
||||
challengeButton = challengeIframeBody.ele("tag:input")
|
||||
# Click the button to trigger the challenge
|
||||
challengeButton.click()
|
||||
```
|
||||
|
||||
## Why It Works
|
||||
|
||||
Cloudflare Turnstile uses behavioral detection to determine if the visitor is human. One signal is the `screenX/screenY` coordinates of mouse events. In headless/automated browsers, these are often 0 or unrealistic values. By patching `MouseEvent.prototype.screenX/screenY` to random values between 800-1200 (x) and 400-600 (y), the behavioral detection passes, and the Turnstile auto-solves without showing a challenge.
|
||||
|
||||
## When to Use
|
||||
|
||||
- When 2captcha fails to solve the profile-page Turnstile (sitekey `0x4AAAAAAAhr9JGVDZbrZOo0`)
|
||||
- When you need faster account creation without external captcha services
|
||||
- The code-page Turnstile (sitekey `0x4AAAAAAADnQ4F4U8W2nQyH`) is also bypassed by this method
|
||||
|
||||
## Requirements
|
||||
|
||||
- DrissionPage library (`pip install DrissionPage==[REDACTED-IP]`)
|
||||
- Chrome/Chromium browser
|
||||
- The Turnstile Patcher Chrome extension (in `turnstilePatch/` directory of the repo) — this patches MouseEvent at the content script level for the initial page load
|
||||
|
||||
## Limitations
|
||||
|
||||
- This only works with DrissionPage (not Playwright) because DrissionPage provides `shadow_root` access
|
||||
- The Chrome extension must be loaded at browser startup
|
||||
- Some Turnstile challenges may still require manual solving if the behavioral detection is more sophisticated
|
||||
Reference in New Issue
Block a user