Skip to content

Latest commit

 

History

History
418 lines (308 loc) · 16.3 KB

File metadata and controls

418 lines (308 loc) · 16.3 KB

ceki-sdk

Using Playwright / Puppeteer / Selenium? Ceki speaks the same CDP protocol — see migration guide

Python SDK for browser.ceki.me — rent real browsers from real people for AI agent automation.

Install

pip install ceki-sdk

Quickstart

import asyncio
import os
from ceki_sdk import connect, ConnectOptions

async def main():
    client = await connect(os.environ["CEKI_API_KEY"])
    options = await client.search({"geo": "US", "language": "en"})
    browser = await client.rent(options[0].schedule_id)
    # ... CDP calls (see docs)
    await browser.close()
    await client.close()

asyncio.run(main())

BREAKING in 2.2.0: connect() no longer accepts relay_url= or reconnect= kwargs — pass a ConnectOptions object instead.

Playwright / Puppeteer / Selenium Compatibility

Ceki speaks raw CDP (Chrome DevTools Protocol) — the same protocol Playwright and Puppeteer use internally. Most automation scripts port with just an import and connection change.

Why Ceki instead of your current stack?

Feature Playwright Puppeteer Selenium Ceki
Real residential IPs
Human behavioral mimicry
Captcha solving via chat
Raw CDP access Partial ✅ native
MCP-native
Real human browser provider
Zero bot detection risk

Per-framework migration

  • Playwrightpage.context.newCDPSession(page)client.rent(), then replace cdp_session.send('DOM.getDocument') with browser.send({"method": "DOM.getDocument"}). Navigation, clicking, typing, and DOM queries work identically.
  • Puppeteerpage._client().send('DOM.getDocument')browser.send({"method": "DOM.getDocument"}). Same CDP method names and params.
  • Seleniumdriver.execute_cdp_cmd('DOM.getDocument', {})browser.send({"method": "DOM.getDocument"}). Same method names, same parameter shapes.
  • undetected-chromedriver — no longer needed. Ceki provides a real human browser fingerprint + residential IP. No WebDriver patches required.
  • selenium-wire — request interception: browser.send({"method": "Network.enable"}) + subscribe to Network.responseReceived events.
  • MCP — Ceki is MCP-native. Connect it as an MCP server (ceki mcp), and any MCP client (Claude Code, Cline, Cursor) gets browser tools — navigate, click, type, screenshot, captcha chat — without writing a single line of CDP code.

Migration example: Playwright → Ceki (Python)

Playwright (raw CDP)Ceki
from playwright.async_api import async_playwright

async with async_playwright() as p:
    browser = await p.chromium.launch()
    page = await browser.new_page()
    cdp = await page.context.new_cdp_session(page)
    await cdp.send("Page.navigate",
        {"url": "https://x.com"})
    await page.click("text=Log in")
    data = await cdp.send(
        "Page.captureScreenshot")
    await browser.close()
from ceki_sdk import connect

async def main():
    client = await connect(API_KEY)
    br = await client.rent(sid)
    await br.send({
        "method": "Page.navigate",
        "params": {"url": "https://x.com"},
    })
    await br.click(320, 480)
    snap = await br.screenshot()
    await br.close()
    await client.close()

The browser.send({"method": ..., "params": ...}) method mirrors Playwright's CDPSession.send() — same CDP method names, same parameter shapes. Only the connection setup changes.

Known Differences

Ceki's CDP-over-WSS relay is not a direct local CDP pipe — a few differences to keep in mind when migrating from Playwright / Puppeteer / Selenium:

  • No CSS selectors on interaction methodsclick, type, and scroll take viewport coordinates, not CSS selectors. For element-based selection, use Runtime.evaluate to get bounding rects, then pass coordinates.
  • Speed — each command travels over the WebSocket relay with ~50–200ms round-trip (internet-latency dependent, unlike local CDP which is sub-millisecond). Batch operations in a single Runtime.evaluate where possible, rather than chaining many small CDP calls.
  • 120s grace window — after each command there's a 120s window before the session auto-closes. This is by design for workflow chaining, but long-running idle sessions will need a keepalive.
  • Runtime.evaluate serialization — CDP's Runtime.evaluate returns JSON-serializable values only. Functions, symbols, DOM nodes, and circular references are not transferable. Use --returnByValue explicitly.

Environment Variables

Variable Description
CEKI_API_KEY Your API key (required)

API

connect(api_key, options: ConnectOptions | None = None) -> Client

Establish a WebSocket connection to the relay. Returns a Client instance.

ConnectOptions

Field Default Description
reconnect True Auto-reconnect on disconnect

client.search(filters=None, limit=20) -> list[BrowserOption]

Search for available browsers. Filters: geo, language, etc.

client.rent(schedule_id) -> Browser

Rent a browser by schedule ID. Waits up to 60s for a match.

client.close()

Close all sessions and the connection.

Error Codes

Exception Cause
AuthFailed Invalid API key or token revoked
RateLimitExceeded Too many requests. Has .retry_after (seconds)
InsufficientFunds Account balance too low
SessionEnded Provider ended the session. Has .reason
CdpUnrecoverable CDP connection lost permanently
ConnectionLost Relay connection lost after max reconnects

Session profile (cookies + storage)

browser.profile lets you snapshot and restore cookies, localStorage, and sessionStorage between sessions — without involving the relay or backend. The blob stays in your own storage.

import json

# First session — sign up, then export profile
async with await client.rent(schedule_id) as browser:
    await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com/login"}})
    # ... perform signup, 2FA ...
    profile = await browser.profile.export(domains=[".reddit.com", "reddit.com"])

with open("reddit_profile.json", "w") as f:
    json.dump(profile, f)

# Next session — restore profile (navigate first, then import storage)
with open("reddit_profile.json") as f:
    profile = json.load(f)

async with await client.rent(schedule_id) as browser:
    # Cookies are domain-scoped — set them before navigation
    await browser.profile.import_(profile)
    await browser.send({"method": "Page.navigate", "params": {"url": "https://reddit.com"}})
    # already logged in

Notes:

  • localStorage/sessionStorage require a document context — navigate to the target origin before calling import_(), or call it right after navigation.
  • Cookies (Network.setCookies) work before any navigation.
  • Use domains to export only relevant cookies and avoid importing third-party trackers.
  • Encrypt the blob before writing to disk if it contains sensitive credentials.
  • import_() raises ValueError on schema_version mismatch (future-proofing).

CDP Lifecycle

The relay maintains the CDP connection to the incognito browser tab. If the connection drops, it automatically reattaches with 1s/2s/4s exponential backoff. Commands during reattach are buffered (FIFO, max 50). If 3 reattach attempts fail, a new fallback tab is created. If that also fails, cdp_unrecoverable error is sent.

Real-signup examples

See examples/SMOKE.md for full runbook.

Quick:

pip install -e ".[dev]"
export CEKI_API_KEY=...
export SCHEDULE_ID=...
python examples/reddit_signup.py

These are NOT automated tests — they require a live relay, an online provider, and a real IMAP mailbox. Run manually as part of Phase 2 acceptance.

Human Mode

Behavioral humanization is ON by default in both main and incognito profile modes:

  • Typing — per-character keystrokes with natural inter-key cadence + jitter (extension-side, Ceki.typeText).
  • Mouse — clicks are preceded by a bezier mousemove trajectory (8–35 intermediate mouseMoved events with per-event timestamps), so the page sees a real pointer trail instead of a teleport.

Fingerprint Tier-2 (User-Agent / timezone / WebGL overrides) stays OFF in main mode to preserve the provider's identity — that's separate from behavioral humanization and not affected by the flags below.

# Default: behavioral humanizer ON (natural profile)
browser = await client.rent(schedule_id)

# Explicit profile
browser = await client.rent(schedule_id, human="careful")

# Disable session-wide humanization
browser = await client.rent(schedule_id, human=None)

# Custom profile dict
browser = await client.rent(schedule_id, human={"typing": {"wpm": 130}})

Per-call disable

Each humanized method accepts human=False for raw, flat behavior on just that call — useful for fast scripted seeding without leaking jitter elsewhere:

await browser.type("user@example.com", human=False)   # flat keystrokes, no jitter
await browser.click(120, 240, human=False)            # straight pointer jump
await browser.scroll(delta_y=-300, human=False)

The CLI equivalent is --no-human / --raw on type, click, scroll, navigate. Both flags mean "this call only".

High-level methods

await browser.navigate("https://example.com")
await browser.click(100, 200)
await browser.type("Hello, world!")  # Ships one Ceki.typeText command; extension fans it out per-char with human delays. Long text no longer trips the relay command cap.
await browser.scroll(delta_y=-300)
img_bytes = await browser.screenshot()

Runtime control

prev = browser.set_human("careful")  # Switch profile, returns previous
browser.set_human(None)               # Disable session-wide humanization

Environment variables

  • CEKI_HUMAN_PROFILE — Override default profile name (e.g., careful)
  • CEKI_HUMAN_PROFILE_PATH — Path to custom JSON profile file
  • CEKI_HUMAN_DISABLE=1Global kill-switch: disable humanization for every call regardless of human=... arguments or CLI flags

CLI

The SDK installs a ceki CLI binary on your PATH.

Install

pip install ceki-sdk

Environment variables

Variable Required Purpose
CEKI_API_KEY yes Agent token (ag_...)

Quick start

export CEKI_API_KEY=ag_...

SCHEDULE=$(ceki search --limit 1 | jq -r '.[0].schedule_id')
SID=$(ceki rent --schedule $SCHEDULE | jq -r .session_id)
ceki navigate $SID https://example.com
ceki snapshot $SID -o snap.png
ceki stop $SID

The CLI persists session state locally — after rent it saves the session ID so subsequent commands resume it by SID without re-renting.

Commands

Discovery and lifecycle

Command Description
search [--limit N] [--filter K=V]… List available browsers
my-browsers List browsers with pre-arranged rent contracts
rent --schedule ID [--mode incognito|main] [--fingerprint-from FILE] Rent a browser
sessions [--all] [--limit N] [--json] List your sessions
stop SID End a session
wait SID Block until the session ends

Browser control

Command Description
navigate SID URL [--no-human|--raw] Open URL (humanized by default; --no-human skips pre/post delays)
click SID X Y [--no-human|--raw] Click at viewport coordinates (mousemove trail ON by default; --no-human for direct jump)
type SID TEXT [--selector CSS] [--no-human|--raw] Type text (humanized by default; --no-human for flat keystrokes)
scroll SID X Y DY [--no-human|--raw] Scroll from (X, Y) by DY pixels (eased by default; --no-human for raw CDP wheel)
screenshot SID -o FILE [--format png|jpeg] [--full] Save screenshot
snapshot SID -o FILE Screenshot + new chat messages
switch-tab SID Switch active tab
upload SID --selector CSS --file PATH [--filename NAME] Attach file to <input type="file">

Chat with host

Command Description
chat SID send TEXT Send message to host
chat SID next [--timeout SEC] Wait for next host message
chat SID history [--since TS] [--limit N] Fetch chat history
chat SID send-image --image PATH [--text MSG] Send image to host

Advanced

Command Description
profile SID export -o FILE [--domains CSV] [--no-session-storage] Export cookies / localStorage
profile SID import -i FILE Import previously exported profile
request-captcha SID [--acceptance SEC] [--completion SEC] [--manual] Ask host to solve CAPTCHA
configure SID [--masking-mode VAL] [--fingerprint VAL] Toggle masking / fingerprint
cdp SID --method METHOD [--params JSON] Raw CDP command

Output and errors

Successful commands write a single JSON line to stdout. Errors go to stderr as {"error": "...", "code": "..."}. Pipe stdout through jq to chain commands.

Exit codes

Code Meaning
0 success
1 generic error
2 CEKI_API_KEY not set
3 session not found or not owner
4 timeout
5 network / connection error
130 interrupted (Ctrl-C)

Full reference (with EN+RU): https://browser.ceki.me/docs#cli

ceki contract — participate in contracts via /mcp/agent

For AI agents executing tasks inside a contract: list contracts/jobs, post results, propose corrections, vote, poll notifications.

ceki contract list                                  # my contracts
ceki contract members <cid>                         # contract members
ceki contract tasks [cid]                           # events of contract(s)
ceki contract my-jobs                               # events assigned to me
ceki contract task <eid>                            # event detail
ceki contract children <eid>                        # event children
ceki contract history <eid>                         # audit history
ceki contract create <cid> --label "X" [--status N] [--type N] \
    [--kal-schedule N] [--start ..] [--end ..] [--date ..] \
    [--duration N] [--amount N] [--currency USD] \
    [--benefitable agent:8|user:61] [--desc ".."]
ceki contract comment <eid> --label ".." [--status N] [--duration N] \
    [--amount N] [--currency USD] [--benefitable agent:8] [--desc ".."]
ceki contract propose <eid> [--status N] [--label ..] [--desc ..] \
    [--duration N] [--amount N] [--currency USD] [--benefitable agent:8]
ceki contract vote <eid> --ids 1,2 --vote true|false
ceki contract poll                                  # single tick (returns [] on 429)
ceki contract watch [sec]                           # continuous (min 6s, 10/min/token)
ceki contract tools                                 # list available MCP tools
ceki contract raw <tool> '<json-args>'              # call any tool directly

Environment

Variable Meaning
CEKI_AGENT_TOKEN Bearer agent token (ag_*). Falls back to CEKI_API_KEY.
CEKI_API_URL Base URL — /mcp/agent and /api/agent/polling are derived from it.
CEKI_AGENT_MCP_ENDPOINT Override MCP endpoint (backward compat).
CEKI_API_BASE Override REST polling base.
CEKI_CONTRACT_IDS Default contract id(s): "14", "14,21", or "[14,21]".

Polling is rate-limited to 10 calls/minute per token; watch enforces a 6s minimum interval.

ceki timelog — event time tracking via /mcp/agent

Top-level group (not under contract). Opens/closes/inspects a UserTime row bound to an event (KalEvent) and the calling agent. Duration on stop is computed server-side; you only pass the optional --label.

ceki timelog start <event_id>                       # timelog-start
ceki timelog stop  <event_id> [--label "что сделал"] # timelog-stop
ceki timelog check <event_id>                       # timelog-check (open log?)

Uses the same env (CEKI_AGENT_TOKEN/CEKI_API_KEY, CEKI_API_URL, CEKI_AGENT_MCP_ENDPOINT) as ceki contract.

Development

pip install -e ".[dev]"
pytest
ruff check ceki_sdk/
mypy ceki_sdk/