215 lines
11 KiB
Markdown
215 lines
11 KiB
Markdown
# AI Provider CDP Registration Patterns
|
|
|
|
Reusable CDP-based registration flows for AI API providers (DeepSeek, OpenRouter, Groq, Mistral, etc.).
|
|
Uses synchronous CDP client with `websocket` + `threading` (from `_reg_v7.py` pattern), not the async `aiohttp` variant.
|
|
|
|
## Synchronous CDP Client (from `_reg_v7.py`)
|
|
|
|
```python
|
|
import urllib.request, json, time, websocket, threading
|
|
|
|
CDP_URL = "http://127.0.0.1:9222"
|
|
|
|
class CDPClient:
|
|
def __init__(self, ws):
|
|
self.ws = ws
|
|
self._id = 0
|
|
self._pending = {}
|
|
self._lock = threading.Lock()
|
|
self._running = True
|
|
self._listener = threading.Thread(target=self._listen, daemon=True)
|
|
self._listener.start()
|
|
|
|
def _listen(self):
|
|
while self._running:
|
|
try:
|
|
self.ws.settimeout(1)
|
|
data = json.loads(self.ws.recv())
|
|
mid = data.get("id")
|
|
if mid is not None and mid in self._pending:
|
|
self._pending[mid] = data
|
|
except: pass
|
|
|
|
def call(self, method, params=None, timeout=15):
|
|
with self._lock:
|
|
self._id += 1
|
|
mid = self._id
|
|
self._pending[mid] = None
|
|
self.ws.send(json.dumps({"id": mid, "method": method, "params": params or {}}))
|
|
start = time.time()
|
|
while time.time() - start < timeout:
|
|
with self._lock:
|
|
val = self._pending.get(mid)
|
|
if val is not None:
|
|
del self._pending[mid]
|
|
r = val.get("result", {})
|
|
inner = r.get("result", {})
|
|
if r.get("subtype") == "error":
|
|
return None, r.get("description", "")
|
|
return inner.get("value"), None
|
|
time.sleep(0.1)
|
|
with self._lock: self._pending.pop(mid, None)
|
|
return None, "timeout"
|
|
|
|
def close(self):
|
|
self._running = False
|
|
self.ws.close()
|
|
|
|
def js(cdp, code, timeout=10):
|
|
return cdp.call("Runtime.evaluate", {"expression": code, "returnByValue": True, "awaitPromise": True}, timeout)
|
|
|
|
# Open new tab
|
|
def open_tab(url=None):
|
|
target = url or "about:blank"
|
|
req = urllib.request.Request(f"{CDP_URL}/json/new?url={target}", method="PUT")
|
|
data = json.loads(urllib.request.urlopen(req).read())
|
|
ws = websocket.create_connection(data["webSocketDebuggerUrl"])
|
|
cdp = CDPClient(ws)
|
|
cdp.call("Page.enable")
|
|
cdp.call("Runtime.enable")
|
|
return cdp
|
|
|
|
# Or reuse existing tab
|
|
def reuse_tab(url_substring):
|
|
tabs = json.loads(urllib.request.urlopen(f"{CDP_URL}/json").read())
|
|
for t in tabs:
|
|
if url_substring in t.get("url", ""):
|
|
ws = websocket.create_connection(t["webSocketDebuggerUrl"])
|
|
cdp = CDPClient(ws)
|
|
cdp.call("Page.enable")
|
|
cdp.call("Runtime.enable")
|
|
return cdp
|
|
return None
|
|
```
|
|
|
|
## React Form Filling
|
|
|
|
React/Next.js apps hijack `HTMLInputElement.prototype.value`. Use native setter:
|
|
|
|
```python
|
|
js(cdp, f"""var inputs = document.querySelectorAll('input');
|
|
for(var i of inputs) {{
|
|
if(i.placeholder === 'Email address') {{
|
|
var s = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
|
s.call(i, '{email}');
|
|
i.dispatchEvent(new Event('input', {{bubbles: true}}));
|
|
i.dispatchEvent(new Event('change', {{bubbles: true}}));
|
|
break;
|
|
}}
|
|
}}""")
|
|
```
|
|
|
|
For React buttons rendered as `<div role="button">`:
|
|
```python
|
|
js(cdp, """var divs = document.querySelectorAll('[role="button"]');
|
|
for(var d of divs) {
|
|
if(d.textContent.trim() === 'Sign up') { d.click(); break; }
|
|
}""")
|
|
```
|
|
|
|
## IMAP Verification Code Polling
|
|
|
|
```python
|
|
import imaplib, email as em, re, ssl
|
|
|
|
ctx = ssl.create_default_context()
|
|
ctx.check_hostname = False
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
|
|
for attempt in range(15):
|
|
time.sleep(4)
|
|
mail = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT, ssl_context=ctx, timeout=10)
|
|
mail.login(email_addr, imap_pw)
|
|
mail.select("INBOX")
|
|
status, msgs = mail.search(None, 'ALL')
|
|
|
|
for mid in reversed(msgs[0].split()[-5:]):
|
|
status, data = mail.fetch(mid, "(RFC822)")
|
|
msg = em.message_from_bytes(data[0][1])
|
|
subj = str(msg['Subject'] or '')
|
|
|
|
body = ""
|
|
if msg.is_multipart():
|
|
for part in msg.walk():
|
|
if part.get_content_type() in ["text/plain", "text/html"]:
|
|
payload = part.get_payload(decode=True)
|
|
if payload: body += payload.decode(errors='replace')
|
|
else:
|
|
payload = msg.get_payload(decode=True)
|
|
if payload: body = payload.decode(errors='replace')
|
|
|
|
body_clean = re.sub(r'<[^>]+>', ' ', body)
|
|
|
|
if provider_name in subj.lower() or provider_name in body.lower():
|
|
codes = re.findall(r'\b(\d{6})\b', body_clean) # 6-digit codes
|
|
links = re.findall(r'https?://[^\s<>"\'\)]+', body) # verification links
|
|
if codes: return codes[0]
|
|
if links: return [l for l in links if provider_name in l.lower()][0]
|
|
|
|
mail.logout()
|
|
```
|
|
|
|
## Provider-Specific Flows
|
|
|
|
### DeepSeek (`platform.deepseek.com`)
|
|
|
|
**Signup flow**: email → verification code (IMAP) → password → API key
|
|
**Page**: `/sign_up` (SPA, React with `ds-` CSS classes)
|
|
**Form fields**: `input[placeholder="Email address"]`, `input[placeholder="Password"]`, `input[placeholder="Confirm password"]`, `input[placeholder="Code"]`
|
|
**Buttons**: `[role="button"]` with text "Send code", "Sign up"
|
|
**Verification**: 6-digit code sent to email, subject "Your verification code for DeepSeek"
|
|
**API key page**: `/api_keys`, key format `sk-<32 chars>`
|
|
**Key creation**: "Create new API key" button → dialog with name input (placeholder "Name of the API key") → "Create API key" → key shown in success dialog with "Copy" and "Done" buttons.
|
|
**Key format**: `sk-<32 chars>` (35 chars total). Regex: `sk-[a-zA-Z0-9]{20,}`.
|
|
**Key extraction**: The key is shown in the success dialog. Read it from `document.body.innerText` before clicking "Done". After dismissal, the table shows a masked version (`sk-62cfe***********************cea5`). The regex `sk-[a-zA-Z0-9]{20,}` on the dialog text captures the full key. The key length is 35 chars — this is correct; the "..." in terminal output is just the actual characters, not truncation.
|
|
**Validation**: Test with `POST https://api.deepseek.com/v1/chat/completions`. A new key returns `402 Insufficient Balance` — this means the key is VALID but needs credits. It will NOT return `401` (which means invalid key).
|
|
**Pitfalls**:
|
|
- Key is only shown once in the success dialog. Intercept before clicking "Done".
|
|
- The masked key in the table is NOT usable — you need the full key from the dialog.
|
|
- New accounts have $0 balance. Top up before using the key.
|
|
**Note**: Click "Sign up" (not "Log in") to switch to registration form. The `/sign_in` page is the login form.
|
|
|
|
### OpenRouter (`openrouter.ai`)
|
|
|
|
**Signup flow**: email → password → (sometimes email verification link) → API key
|
|
**Page**: `/sign-up` (Clerk.js modal)
|
|
**Form fields**: `input[name="firstName"]`, `input[name="lastName"]`, `input[name="emailAddress"]`, `input[name="password"]`, `input[name="legalAccepted"]` (checkbox)
|
|
**Buttons**: `button` with text "Continue"
|
|
**Verification**: Sometimes email verification is required (link sent to inbox), sometimes not. With `[REDACTED-EMAIL]`, signup succeeded without verification — redirected straight to `/`. When verification IS required, the page is `/sign-up/verify-email-address` with a "Resend" countdown timer.
|
|
**API key page**: `/keys` → `/workspaces/default/keys`. Key format `sk-or-v1-<uuid>`.
|
|
**Onboarding wizard**: After first login, OpenRouter shows a "How will you be using OpenRouter?" wizard (Individual/Organization). Click "Individual" → "Next" to proceed. If stuck, navigate directly to `https://openrouter.ai/settings/keys`.
|
|
**Password reset**: Use "Forgot password" → "Email link" → check IMAP for reset link → set new password.
|
|
**Pitfalls**:
|
|
- "That email address is taken" error if email already registered.
|
|
- "Enter your password" second step (Clerk.js factor-one) if account exists but wrong password.
|
|
- **Stripe fraud detection captures CDP tabs**: After navigating to the keys page, Stripe (`m.stripe.network`) takes over the tab with an empty iframe. The CDP browser has no real user interaction history, so Stripe flags it. Workaround: open a fresh tab and navigate directly to `https://openrouter.ai/settings/keys`. If Stripe still captures, use the user's real Chrome browser (not CDP) for OpenRouter key extraction.
|
|
- Use fresh email from pool. Reuse working emails (davidlemnitzer43, annett.riebe) across multiple providers — one email can serve DeepSeek + OpenRouter + others.
|
|
|
|
### Groq (`console.groq.com`)
|
|
|
|
**Signup flow**: email → magic link (IMAP) → API key
|
|
**Pitfall**: Magic links do NOT arrive on t-online.de emails. Groq likely blocks or rate-limits t-online.de. Use gmail.com or other providers.
|
|
**Page**: `/home` — email input + "Continue with email" button
|
|
**Key page**: `/keys`, key format `gsk_<chars>`
|
|
|
|
### Mistral (`console.mistral.ai`)
|
|
|
|
**Signup flow**: email → password → API key
|
|
**Page**: `/login` (redirects to `v2.auth.mistral.ai`)
|
|
**Pitfall**: "Please select a way to login" → enter email first, then password. If account exists, asks for password. If not, shows signup flow.
|
|
|
|
### Token Harbor (`tokenharbor.ai`)
|
|
|
|
**Key extraction**: Dashboard at `/dashboard/api-keys`. Keys are `thk_live_<chars>`. "Show" button triggers email verification (6-digit code to `[REDACTED-EMAIL]`). Verification code sent from `verify@tokenharbor.ai`.
|
|
**Pitfall**: Key reveal requires 2FA email code. The "Send verification code" → "Reveal key" flow works. Code arrives as 6-digit code in email.
|
|
|
|
## Pitfalls
|
|
|
|
1. **t-online.de emails blocked by Groq**: Magic links never arrive. Use gmail.com or other email providers.
|
|
2. **IMAP pool passwords are mostly invalid**: Only ~3 of 17K+ tested emails had working IMAP passwords. The initial IMAP4_SSL + login() test can produce false positives (connects but does not verify). Always test with mail.select("INBOX") + mail.search(None, 'ALL') to confirm the login actually works. After a confirmed working email, reuse it across multiple providers — one email can serve DeepSeek + OpenRouter + others.
|
|
3. **DeepSeek key is masked after creation**: The full key is only shown in the success dialog. Intercept it before clicking Done. The key is valid even if it returns 402 Insufficient Balance — new accounts have $0.
|
|
4. **OpenRouter uses Clerk.js modal**: The signup form is in a modal, not a separate page. The /sign-up URL may redirect to /sign-in with a modal. Click Sign up link in the modal.
|
|
5. **OpenRouter Stripe interference**: After navigating to the keys page, Stripe (m.stripe.network) captures the CDP tab with an empty iframe. The CDP browser lacks real user interaction history, triggering fraud detection. Open a fresh tab and navigate directly to https://openrouter.ai/settings/keys. If Stripe persists, use the user real Chrome browser.
|
|
6. **React div[role=button] buttons**: Not button elements. Use [role=button] selector.
|
|
7. **CDP tab URL parameter may not work**: PUT /json/new?url=... sometimes opens about:blank. Use Page.navigate after opening the tab.
|
|
8. **MCP Chrome DevTools blocked by CloudFront**: DeepSeek returns 403 via CloudFront for MCP browser. Use user real Chrome (CDP on port 9222) instead. |