Files

11 KiB

Conol.ai Pool Gateway Pattern

Date: 2026-07-29 Status: Production — 21 accounts, public ngrok endpoint

Architecture

Multi-account OpenAI-compatible API gateway that round-robins across conol.ai accounts using the internal agent session protocol. Each request creates a fresh Conol agent session, streams the response via SSE, and returns OpenAI-format output.

Why Pool Instead of Single Account

  • Rate limits: Single account hits agent session creation limits
  • Credits: Each free account has ~800 credits (2000 after quest farming), shared pool extends total budget
  • Redundancy: Failed accounts auto-disabled, requests route to healthy accounts
  • Model diversity: Different accounts may have different model availability tiers

Core Protocol: Conol Agent Sessions

Conol.ai does NOT have a public REST API for chat. Instead, it uses an internal agent session protocol:

1. POST /api/sessions           → {sessionId}
   Body: {source, messages, timezone, agentModel, agentEffort}
   
2. GET /api/sessions/{id}/messages?logDeltas=1   → SSE stream
   Events: assistant, stream_event (text_delta), history_delta, thinking, 
           tool_use, plan, done, error, status, questionary
           
3. Extract final response text from history_delta → stages → logs → content

Key Implementation Detail

The SSE stream's history_delta event contains the final agent response nested inside stages[].logs[].content[]. Each content part has type: "text" and the text field.

async with client.stream("GET", f"{BASE_URL}/api/sessions/{sid}/messages?logDeltas=1") as stream:
    async for line in stream.aiter_lines():
        ev = json.loads(line[5:].strip())
        if ev.get("type") == "done":
            break
        if ev.get("type") == "history_delta":
            for stage in ev.get("stages", []):
                for log in stage.get("logs", []):
                    for part in log.get("content", []):
                        if part.get("type") == "text":
                            full_text = part["text"]

Auth

Better Auth session token in cookie __Secure-better-auth.session_token. No Bearer token — cookies-only.

Session Cookies

Persist cookies from account registration. Each account's cookies are stored in cookies_conol<name>.json. Load as dict {name: value} and pass to httpx.AsyncClient.

Model List (18 models, July 2026)

gpt-5.5, gpt-5.5-pro, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna
deepseek/deepseek-v4-pro, deepseek/deepseek-v4-base
kimi/kimi-k3, qwen/qwen-3.7, glm/glm-5.2
claude-opus-4-8 ✦, claude-fable-5 ✦, claude-opus-4-7 ✦
gemini-3-pro ✦, gemini-3-flash ✦
llama-4-maverick, llama-4-scout, mistral-large-3

✦ = vision-capable (image input)

Image Generation

Claude models (opus-4-8, fable-5) can generate SVG images when prompted with "Generate an SVG image...". They do NOT generate pixel images (PNG/JPEG). The /v1/images/generations endpoint wraps this behind an OpenAI-compatible API.

Output: SVG in response text. Example result: 2384-byte neon skull SVG with glow filter.

For real PNG/JPEG generation: need cairosvg/resvg for SVG→PNG conversion, or external image-gen API.

Round-Robin with Health Checks

class PoolManager:
    accounts: list[Account]  # from conol_accounts_pool.jsonl
    _idx: int                # atomic round-robin index
    
    async def get_next(self) -> Account:
        # Skip accounts with fail_count >= 3
        # Cycle through active accounts
        # Auto-reset fail_count when all accounts failing

Accounts with 5 consecutive failures are marked active=False.

Gateway Endpoints

Endpoint Method Description
/health GET Pool status (active/total, per-account stats)
/v1/models GET OpenAI-compatible model list (18 models)
/v1/chat/completions POST Chat with stream: true/false
/v1/images/generations POST SVG generation via Claude
/pool/stats GET Detailed per-account statistics
/pool/reload POST Hot-reload accounts from jsonl

Streaming

The gateway supports SSE streaming (OpenAI-compatible stream: true). Since Conol's SSE is already streaming, the gateway chunks the full response into 16-char segments and yields OpenAI-format delta chunks.

Public Exposure via ngrok

ngrok http http://127.0.0.1:9999 --url https://<static-domain>.ngrok-free.dev

Static ngrok domain ensures consistent public URL across restarts. Ngrok rewrites Host header and bypasses browser warnings.

Account Management

Registration (Chrome CDP)

python mass_reg_conol.py <target_count>

Uses Chrome DevTools Protocol (port 9228) to automate conol.ai signup:

  1. Navigate to /sign-up
  2. Execute reCAPTCHA v3 via grecaptcha.execute(siteKey, {action})
  3. POST /api/invites/register with x-captcha-response header
  4. Send verification email
  5. IMAP poll for verification link
  6. Login + extract cookies + API key

Success rate: 13/15 (87%) with increasing delays (8s → 50s). Rate limit: IP-based, hits every ~4th account (HTTP 429 on verify-send).

reCAPTCHA v3 Details

  • Type: Google reCAPTCHA v3 (NOT Cloudflare Turnstile)
  • Site key: 6Lc3wmAtAAAAAB9YBPXQtT9uGGsH3ul6LQBc5AUu
  • Actions: sign_up, send_verification_email, sign_in
  • Domain-locked: 3rd-party captcha solving services (AntiCaptcha, 2Captcha) cannot solve it — the reCAPTCHA server rejects their tokens. Browser-based execution via Chrome CDP is the only working path.
  • Headless penalty: reCAPTCHA v3 scores headless Chrome at ~0.1 (threshold 0.5). Real GUI Chrome required.

Quest Farming

python quest_farmer.py

Automatically completes quests via agent sessions for +1200 credits per account:

  • note_written_by_agent — ✅ works
  • memory_written — ✅ works
  • note_edited_by_agent — ✅ works
  • timer_scheduled — ✅ works
  • note_shared — ❌ requires authenticated Conol session
  • skill_installed — ✅ works (installs skills like find-skills, deep-research)

Post-farm: each account has ~2000 credits (800 base + 1200 quests).

Account Storage

conol_accounts_pool.jsonl   — JSONL records (email, password, name, cookies_path, credits, api_key_masked)
cookies_conol<name>.json    — Session cookies per account
conol_pool_state.json       — Mass registration state
conol_quests_state.json     — Quest farming state

Captcha Key Verification (2026-07-29)

Provider Keys Status
2Captcha (old API) 18 keys ❌ All ERROR_KEY_DOES_NOT_EXIST on V2 API
AntiCaptcha 4 keys ✅ Live ($45.45 total)
CapSolver 1 key ❌ $0.0002 (effectively empty)

Key takeaway: 2Captcha old API keys (verified via res.php?action=getbalance) do NOT work with the new API (api.2captcha.com/createTask). Always verify keys against the actual API endpoint you'll use, not just the balance check.

Deployment

ENI Conol Pool Gateway v3.0
Port: 9999
Processes: chrome.exe (CDP), conol_pool_gateway.py, ngrok.exe
Persistence: watchdog + autostart via .vbs

Watchdog monitors gateway health and restarts on failure. Ngrok auto-reconnects with static domain.

When validating that all accounts in the pool are alive, use this pattern — NOT the gateway (which hides per-account errors behind round-robin):

import json, httpx, asyncio

async def check_account(acc):
    email = acc['email']
    cp = acc['cookies_path']
    with open(cp) as f:
        cookies = {c['name']: c['value'] for c in json.load(f)}
    
    # ⚠️ CRITICAL: каждый аккаунт ДОЛЖЕН иметь СВОЙ httpx.AsyncClient.
    # Общий клиент на все аккаунты перемешивает куки → все получают 401.
    async with httpx.AsyncClient(cookies=cookies, timeout=httpx.Timeout(30)) as client:
        r = await client.post('https://conol.ai/api/sessions', json={
            'source': {'type': 'home'},
            'messages': [{'type': 'text', 'content': 'Say OK'}],
            'timezone': 'Europe/Kyiv',
            'agentModel': 'gpt-5.5',
            'agentEffort': 'low',
        })
        
        if r.status_code != 201:
            return (email, False, f'HTTP {r.status_code}')
        
        sid = r.json()['sessionId']
        # Stream SSE and look for history_delta (NOT text_delta!)
        async with client.stream('GET', f'https://conol.ai/api/sessions/{sid}/messages?logDeltas=1') as stream:
            async for line in stream.aiter_lines():
                if not line.startswith('data:'):
                    continue
                ev = json.loads(line[5:])
                if ev.get('type') == 'done':
                    return (email, True, 'ok')
                if ev.get('type') == 'history_delta':
                    for stage in ev.get('stages', []):
                        for log in stage.get('logs', []):
                            for c in log.get('content', []):
                                if c.get('text'):
                                    return (email, True, 'ok')
        return (email, False, 'no done event')

# Run with semaphore for rate limiting
sem = asyncio.Semaphore(3)  # max 3 concurrent
async def bounded(acc):
    async with sem:
        return await check_account(acc)

results = await asyncio.gather(*[bounded(a) for a in accounts])
ok = [r for r in results if r[1]]   # (email, success, detail)
fail = [r for r in results if not r[1]]

Key Pitfalls

  1. Shared httpx.AsyncClient → ALL accounts return 401. If you create ONE httpx.AsyncClient(cookies=cookies_a) and reuse it for account B's requests, httpx merges the cookies and sends the wrong session token. Every concurrent test MUST create a fresh, isolated httpx.AsyncClient per account.

  2. SSE event type is history_delta, NOT text_delta. The conol.ai SSE stream uses history_delta — a nested history log with stages[] → logs[] → content[]. Looking for text_delta (like OpenAI SSE format) gives "empty response" even though the session created successfully (HTTP 201).

  3. What a "healthy" account looks like:

    • POST /api/sessions returns HTTP 201 (NOT 200, NOT 401)
    • SSE stream eventually delivers type: "done" or history_delta with content
    • HTTP 401 = session expired, needs re-login via Chrome CDP
    • HTTP 429 = rate limited, retry with delay
  4. Auth cookies: Better Auth tokens: __Secure-better-auth.session_token and __Secure-better-auth.better-auth-passkey. Both REQUIRED. Cookie files contain exactly 2 entries per account. If a cookie file has different structure, the session is from an old auth mechanism and won't work.