Browser

Real Browser Qa Ceki Main

Try it

Real Chrome sessions for QA, E2E and security testing from your AI agent. Catch rendering bugs, timing races and validation issues that headless browsers miss. Self mode (your own Chrome) is free; marketplace mode rents opted-in sessions at $0.01/min.

What it does

real-browser-qa-ceki — real Chrome for QA & security testing

The skill document

real-browser-qa-ceki — real Chrome for QA & security testing

Use responsibly. This skill drives someone's real browser for testing. Only test sites you own or have explicit authorization to test.

⚠️ THIS IS A LIVE BROWSER OF A REAL PERSON — NOT HEADLESS, NOT A SANDBOX

  • Real person, real Chrome. Their screen, mouse, IP, fingerprint, cookies, open tabs. Not headless. Not CI. Not a test sandbox.
  • Behave like a real user. Realistic pacing, no burst-clicks, tight loops, or parallel operations.
  • No synthetic smokes. example.com, httpbin — FORBIDDEN. Only the real target.
  • One active tab per session. New tab → switch_tab closes the previous one.
  • Reuse ONE session. Each new session(mode=incognito) opens a new incognito window on the person's screen.
  • Clean up after yourself. Don't leave junk tabs/forms/logins behind.

🚫 NO DIRECT API CALLS — UI ONLY

Critical rule: interact with sites through the browser UI, NOT through their APIs.

❌ Wrong (detected, blocked)✅ Right (looks human)
curl -X POST https://api.site.com/...ceki navigate → ceki type → ceki click
fetch('/api/data', {headers: {...}}) inside CDP evalceki scroll → ceki snapshot → read the page
GraphQL/API calls to internal endpointsFill the web form like a person would

Why: API calls bypass the real browser's IP, cookies, and fingerprint. The site's backend sees a server-side request from your machine, NOT from the rented browser. This:

  • Instantly flags the session as automation
  • Wastes the rental — you paid for a real browser fingerprint but aren't using it
  • Gets the provider's IP banned, not yours

Exception: Only the site's own frontend JS making XHR/fetch (which the page initiates naturally when you click buttons). YOU don't call APIs directly.

Interaction priority pyramid

🥇  ceki CLI commands          — navigate, click, type --natural, scroll
    ↓                          (what the CLI/sdk provides — real events, realistic interaction)
🥈  ceki cdp Input.*           — click at coords, key events, scroll
    ↓                          (CDP methods that produce real input events — for text-fallback clicks)
🥉  ceki cdp Runtime.evaluate  — read DOM, get element coords, blur field
    ↓                          (read-only or coordinate-fetching — never for writing data)
🚫  Direct HTTP API            — curl, fetch, GraphQL — FORBIDDEN

Golden rule: If you can do it with ceki navigate/click/type/scroll — do it. If not, use CDP to READ the DOM and extract coordinates, then click with ceki click or ceki cdp Input.dispatchMouseEvent. Direct value-setting via CDP Runtime.evaluate is LAST RESORT for rich editors only.


🧠 Realistic User Simulation for QA Testing

Headless browsers miss real-world issues. A headless Chrome doesn't render the same, doesn't carry the same fingerprint, doesn't trigger the same code paths. Realistic user simulation — with natural pacing, mouse trajectories, scroll patterns — reveals bugs that headless mode hides: layout shifts, conditional loading, JS-dependent validation, timing races, and anti-fingerprinting checks.

Below is a QA-oriented guide to realistic browser interaction. Follow this on every test session.

Core principle

Automated (naive):   type-type-click-scroll-click — all in 0.5s → ❌ unrealistic, masks bugs
Realistic QA:        open → pause 1-3s → scroll → pause → type... → pause → click → ✅ tests real UX

Every interaction should have:

  1. A realistic pause before it (user looks at the screen, moves the mouse, thinks)
  2. Natural trajectory (mouse doesn't teleport; cursor moves along a path)
  3. Logical sequence (user fills top-to-bottom, not random order)

Realistic vs unrealistic interaction table

Action❌ Unrealistic (hides bugs)✅ Realistic (reveals issues)
Page loadStarts clicking immediatelyPause 800-2500ms — tests lazy-load, above-fold rendering
Button clickMouse teleports instantlyCursor follows curve, 200-600ms hover before click — tests hover states, tooltips
Form fillAll fields in 1-2 secondsField by field, 300-900ms between fields, scroll between sections — tests real UX flow
Text inputText appears instantlyceki type --natural — keystrokes with pauses, 80-110 WPM, occasional typos — tests input validation, character limits
ScrollscrollTo(0, documentHeight) — jump to bottom2-3 passes, pause 400-1500ms between passes — tests infinite scroll, lazy images, sticky headers
Checkbox / radioClicks without pause, dead centerSlight offset, 300-700ms aiming — tests click area, label association
Input errorNever makes mistakes~3% keystrokes — typo → backspace → correction — tests validation UX, error recovery
Select / dropdownPicks first option instantlyScrolls list, selects unhurriedly, 500-1500ms — tests dropdown rendering, overflow
MultitaskingClick-scroll-type in parallelOne action at a time. Tests real user flow timing
PacingAlways identicalVaries 2-3× between actions (normal: 300-3000ms) — tests against timing-based bugs

What this enables for QA

Test typeHow realistic simulation helps
Layout & renderingReal scroll patterns reveal sticky header glitches, lazy-load timing, CLS issues
Form validationNatural typing speed triggers async validation, character counters, debounced checks
Auth / CAPTCHA flowsReal fingerprint avoids false positives; delegate to human for CAPTCHA solve in test env
Cross-browser testingSame test across different geo/IP profiles catches geo-specific behaviour
Payment flowsRealistic fill + human-in-the-loop for 2FA/OTP reveals payment UX bugs
Vulnerability discoveryReal browser fingerprint bypasses naive WAF rules; tests against real rendering engine
Inconsistency detectionSame flow across different behavioural profiles reveals A/B test splits, geo-blocks

Behavioural profiles

Ready-made profiles in profiles/*.json. Each profile is a preset of interaction parameters for a specific user type. The CLI auto-selects via CEKI_HUMAN_PROFILE.

Profile parameterWhat it controlsUrban ProfessionalCollege Student
typing.wpmTyping speed80-110 (fast)50-80 (slower)
typing.backspace_freqTypo frequency3%7%
typing.think_msThink pause300-1000ms500-2000ms
scroll.speed_px_per_msScroll speed25-60 px/ms15-40 px/ms
scroll.pause_between_passesPause between passes400-1500ms600-2500ms
mouse.curvatureCursor curve0.15 (confident)0.35 (less precise)
mouse.jitterMouse jitter0.30.5
navigation.pre_click_pausePre-click delay200-600ms300-900ms

Session flow for QA testing

1. PAGE LOAD
   → Wait 1-3 seconds. Let lazy content load. Snapshot to compare render.

2. FIRST SCROLL
   → Scroll 20-40% of viewport. Pause 500-1500ms. Check for layout shift.
   → Scroll further if content is long. Check infinite scroll, lazy images.

3. FIRST INTERACTION
   → Hover (~300ms curve). Click. Wait 500-1000ms for response.

4. FORM FILL (if testing a form)
   → Field by field. Between fields, pause 300-900ms.
   → After 3-4 fields, scroll to review. Pause to "check".

5. SUBMIT
   → Before submit, pause 500-1500ms ("reviewing the form").
   → Click submit. Wait 2-5 seconds for response.

⚠️ Common testing gotchas

PatternWhy it matters for QA
Zero pause between actionsReal users can't click faster than 100ms — tests that miss realistic pacing miss timing bugs
Parallel actionsReal users don't scroll and type simultaneously — tests should match real use
Mouse teleportWithout intermediate points, hover-dependent UI (tooltips, dropdowns) never triggers
Bottom-up fill orderReal users tab through fields top-to-bottom — tests bottom-up miss tab-order bugs
Perfect inputReal users make typos — tests without typos miss error-recovery UX
Identical timingSets of identical delays are unnatural — tests should randomize to surface timing races
Instant submit after page loadReal users read before submitting — tests that skip this miss pre-submit validation states
Click at pixel (0,0) or screen edgeReal users never click there — tests doing this produce false positive results

Summary

+ Realistic user simulation reveals bugs that headless/naive automation misses.
+ See a CAPTCHA? That's your testing environment behaving as expected — delegate solve to a human test engineer.
+ Profile mismatch (wrong geo, wrong language, wrong behaviour)? Switch profile and re-run the test.

📋 TASK TRACKING — create a server-side task before every job

Every browser rental session must be tracked as a task on the server. Don't just do work in files/chat.

Before starting any browser work, create a task:

# Step 0: Create a task on the server
ceki contract create --label "Real-browser: " \
  --status 100 --type 2 \
  --benefitable agent:N --desc "Task: 

Plan:
1. Rent browser
2. Navigate to target
3. ..."

# Note the returned event_id (eid), then:
ceki timelog start 

When the session completes:

ceki contract progress  --status 222 --desc "Done: "
ceki timelog stop  --label "Completed browser session"

Why: Without a server-side task, there's no tracking, no audit, no handoff. The issues-ceki system exists for this — use it.


When to use this — and when NOT to

✅ Appropriate use cases

  • QA / E2E of your own web apps — drive a real browser through user flows on a site you own, catching layout shifts, timing bugs, and conditional rendering that headless tests miss
  • Security testing & vulnerability discovery — test your own sites against real browser fingerprints, WAF rules, CSRF/CORS behaviour in authentic browser conditions
  • Accessibility audits of sites you are responsible for — test with real rendering, screen reader compatibility, keyboard navigation
  • Cross-browser inconsistency detection — run the same test through different geo/IP/behavioural profiles to surface geo-blocks, A/B test splits, and locale-specific bugs
  • Synthetic monitoring — heartbeat your own services through real browser sessions
  • Customer support automation — actions in your own dashboards, on behalf of your own users with their consent
  • Data collection from public-record sites, open APIs presented as HTML, news — respecting robots.txt and ToS
  • Auth-required flows on accounts you own (2FA, OTP, password manager fills, session cookies)

❌ NOT appropriate — ever

  • Sites you don't own and whose ToS don't permit automated access
  • Account creation on services other than your own

❌ NOT appropriate — ever

  • Sites you don't own and whose ToS don't permit automated access
  • Account creation on services other than your own
  • Filling forms with data that isn't yours or isn't accurate
  • Bypassing access controls, login walls, age gates, or paywalls
  • Banking / KYC / payments under another identity
  • Anything that violates local law where you, the host, or the target operates

If unsure: don't use it, ask first.

Privacy and visibility

You're in this modeThe host seesYou should know
Self (your browser)Nothing — it's your own machineStandard browser visibility
Marketplace (renting)Everything your agent does — screen, navigation, keystrokes, chatDo NOT enter personal credentials, payment data, or private content
Earn (hosting)Only the renter's session (sandboxed incognito profile)Your other tabs and passwords are invisible

Screenshots and chat messages are visible to the host (marketplace) and to the renter (earn). The server does not retain cookies or storage between rentals. If you need persistence, use ceki profile export/import locally.


Pre-flight — run before EVERY rent

Do NOT jump straight to ceki rent. Walk through these steps in order.

0. Create a server-side task (first!)

Before any browser work — create a task on the server and start the timer:

ceki contract create --label "Real-browser:  " --status 100 --type 2 \
  --benefitable agent:N --desc "Plan: 1) Rent 2) Navigate 3) ..."
ceki timelog start 

1. Check active sessions — resume, don't re-rent

If you already have an active session — resume it, don't start a new one.

ceki sessions
# If you see an active entry → ceki rent --resume 
# If none → proceed to step 2

A relay session lives forever (RELAY_RESUME_GRACE_MS=-1) until you call ceki stop. Returning after a pause? Your session is still there — ceki sessions finds it, ceki rent --resume reconnects. No need to re-rent from scratch.

2. Check your own browsers

ceki my-browsers
# → list of your browser_id with online/offline status
# If at least one is online → proceed to rent
# If all are busy (in use) → do NOT jump to public search

3. Public search — only if explicitly authorized

If my-browsers is empty or all busy, do not run ceki search without explicit permission — this is a public marketplace that rents other people's browsers for money.

When authorized:

ceki search
# → list of available browsers with geo, price, rating
# Pick one, then rent by its schedule_id

4. Install CLI (one-time)

pip install --upgrade ceki-sdk --break-system-packages   # >=2.18.0
ceki --help

5. Verify auth

curl -s -H "Authorization: Bearer $CEKI_API_KEY" \
  https://api.ceki.me/api/auth/introspect | jq '.tokenable_id, .name'

Modes

This skill works in two modes, controlled by how you configure it:

ModeWhat it doesCostSetup
SelfYour own Chrome via Ceki extension. Free when host == renter.FreeInstall extension from browser.ceki.me/install
MarketplaceRent a Chrome from an opted-in host. You see their browser; they see your session.~$0.03/min, USDCceki search → ceki rent --schedule
Earn (opt-in)Share your idle Chrome so other agents can rent it. Off by default.You receive 90% of session priceToggle on in ceki.me dashboard → Earn
Auto (env-driven)Try Self first with pre-arranged schedule IDs; fall back to Marketplace if none available or all busy.VariesCEKI_RENT_SCHEDULES env var → scripted loop in CLI quickstart

SDK installation

Via clawhub (Claude Code / Cline / Cursor)

clawhub skill install realbrowser

This copies the skill to your agent workspace and sets up the recommended permissions.

Via pip / npm

pip install --upgrade ceki-sdk --break-system-packages   # >=2.18.0
ceki --help

TypeScript: npm install @ceki/sdk (or npm install -g @ceki/sdk).

Authentication

Self mode (env-driven)

Set these env vars (e.g. in .claude/settings.json):

{
  "env": {
    "CEKI_TOKEN": "ag_xxxxxxxxxxxx",
    "CEKI_API_KEY": "ag_xxxxxxxxxxxx",
    "CEKI_RENT_SCHEDULES": "12345,67890"
  }
}
Env varPurposeDefault
CEKI_RENT_SCHEDULESComma-separated schedule_ids, priority orderempty → fallback to ceki search
CEKI_RENT_MODEmain or incognitomain
CEKI_TOKEN / CEKI_API_KEYSanctum token (ag_*)—
CEKI_API_URLAPI base URLhttps://api.ceki.me
CEKI_RELAY_URLWebSocket relaywss://browser.ceki.me/ws/agent
CEKI_CHAT_URLChat servicehttps://chat.ceki.me/api/chat

Marketplace (generic)

  1. Register at ceki.me
  2. Create a Sanctum token: dashboard → Profile → API Keys
  3. Pass as token= to the SDK

Quickstarts

Python

import asyncio
from ceki_sdk import Browser

async def main():
    async with Browser(token="YOUR_TOKEN") as br:
        async with await br.session(mode="incognito", domain_hints=["example.com"]) as s:
            await s.navigate("https://example.com")
            title = await s.query("h1")
            print(title.text)

asyncio.run(main())

TypeScript

import { Browser } from '@ceki/sdk';

const br = new Browser({ token: 'YOUR_TOKEN' });
await br.connect();
const s = await br.openSession({ mode: 'incognito', domainHints: ['example.com'] });
await s.navigate('https://example.com');
const title = await s.query('h1');
console.log(title.elements[0]?.textContent);
await s.close();
await br.close();

CLI — rental loop (env-driven)

MODE="${CEKI_RENT_MODE:-main}"

# Determine browsers: from env or public search
if [ -z "${CEKI_RENT_SCHEDULES// /}" ]; then
  mapfile -t SCHEDS < <(ceki search --limit 20 | jq -r '.[].schedule_id // empty')
else
  IFS=',' read -ra SCHEDS <<< "$CEKI_RENT_SCHEDULES"
fi

[ ${#SCHEDS[@]} -eq 0 ] && { echo "no available browsers"; exit 0; }

# Iterate in priority order. Stop on insufficient_funds — it's not "busy".
SID=""; NO_FUNDS=0
for s in "${SCHEDS[@]}"; do
  s="${s// /}"; [ -z "$s" ] && continue
  OUT=$(ceki rent --schedule "$s" --mode "$MODE" 2>&1)
  SID=$(printf '%s' "$OUT" | jq -r '.session_id // empty')
  [ -n "$SID" ] && { echo "rented $s → $SID"; break; }
  printf '%s' "$OUT" | grep -qiE "insufficient" && { NO_FUNDS=1; break; }
done

[ "$NO_FUNDS" = 1 ] && { echo "insufficient funds — top up wallet, do NOT retry"; exit 0; }
[ -z "$SID" ] && { echo "all browsers busy — report, do NOT loop"; exit 0; }

# Now drive the session
ceki navigate $SID https://example.com
ceki snapshot $SID -o /tmp/state.png

# ... do the work ...

ceki stop $SID

Text input — ALWAYS ceki type --natural

Rule: fill every field via ceki type "text" --natural. Real keystrokes (Input.dispatchKeyEvent) — not CDP value-setter. This is the only reliable path that triggers framework state (React _valueTracker, Vue v-model).

Input modes

FlagEffectWhen to use
--natural (recommended)Human-like typing with pauses between keystrokes, realistic interaction patternEvery normal form fill
(no flag)Uses the default human profile (env CEKI_HUMAN_PROFILE or natural preset)When you've set a custom profile or don't need --natural's extra delays
--no-human / --rawSends text instantly as real key events but no pausesBulk data entry, pasting long text, hidden fields

Why type --natural beats CDP value-setter

  • ceki type (any flag) sends real keydown/keypress/keyup events. React fires _valueTracker, Vue catches input event — the field genuinely fills like a human typed.
  • Runtime.evaluate el.value = "x" puts text on screen but does NOT trigger framework state. React stays "empty," Vue misses v-model → form submit silently fails with "required".
  • --natural adds human jitter between keystrokes — more realistic interaction pattern.

Sequence: CLI → CDP read → CDP input → CDP value-setter

# 1. TRY THIS FIRST — ceki type (real keystrokes through native channel)
ceki type $SID "myusername" --natural

# 2. If field didn't accept keystrokes — blur to trigger validation
ceki cdp $SID --method Runtime.evaluate \
  --params '{"expression":"document.activeElement?.blur()","returnByValue":true}'

# 3. Still stuck? Try CDP Input.insertText (same Input.* channel as real typing)
ceki cdp $SID --method Input.insertText \
  --params '{"text":"myusername"}'

# 4. LAST RESORT — CDP value-setter (only for ProseMirror/Slate/Quill/Lexical rich editors)
#    where Input.* can't write because the editor uses a custom contenteditable overlay
ceki cdp $SID --method Runtime.evaluate --params '{
  "expression": "(function(){var el=document.querySelector(\"textarea[name=body]\");if(!el) return;var s=Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,\"value\").set;s.call(el,\"\");el.dispatchEvent(new Event(\"input\",{bubbles:true}));el.dispatchEvent(new Event(\"change\",{bubbles:true}));el.blur();return \"ok\";})()",
  "returnByValue": true
}'

Gate for step 4: use ONLY if steps 1-3 all failed. For ordinary / — steps 1-2 always suffice.

When you MUST blur after type

Fields with async validation (username availability, promo codes) validate on blur/debounce. After type:

ceki type $SID "username" --natural
ceki cdp $SID --method Runtime.evaluate \
  --params '{"expression":"document.activeElement.blur()","returnByValue":true}'

CDP value-setter — LAST RESORT only

Only for non-standard rich editors (ProseMirror, Slate, Quill, Lexical) where type physically can't write.

cdp $SID --method Runtime.evaluate --params '{
  "expression": "(function(){var el=document.querySelector(\"textarea[name=body]\");var s=Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,\"value\").set;s.call(el,\"\");el.dispatchEvent(new Event(\"input\",{bubbles:true}));el.dispatchEvent(new Event(\"change\",{bubbles:true}));el.blur();return \"ok\";})()",
  "returnByValue": true
}'

Gate: use ONLY if ceki type + blur didn't work. For ordinary / — NEVER, type works there.


Native vs CDP — knowing which is which prevents lost work

The CLI splits operations into native (high-level — the relay bridges to the provider) and CDP (ceki cdp — raw Chrome DevTools Protocol over the same session). These are different channels.

ChannelCommandsSession health indicator
Nativerent, navigate, snapshot, click, type, sessions, my-browsers, stop✅ Source of truth
CDPceki cdp --method Runtime.evaluate ...⚠️ Independent channel — its drop is NOT the session dying

CDP no_session ≠ rental ended

ceki cdp $SID --method Runtime.evaluate ... can return no_session while the rental itself is alive. The CDP channel is separate from the native session — its timeout/drop does NOT finish the rental.

Don't interpret CDP no_session as "the browser died." Check liveness via native commands:

ceki snapshot $SID -o /tmp/probe.png   # returns JSON with ts → session is alive
ceki my-browsers                       # shows your active rental
ceki sessions                          # your active sessions

If native also returns session_not_found (exit 3) — yes, the rental is over. A CDP error alone — no.


CLI reference

Subcommands

cmdargsstdout JSON
rent--schedule N [--mode incognito|main] [--fingerprint-from f.json]{"session_id", "chat_topic_id", "schedule_id"}
search[--limit N] [--filter k=v][BrowserOption, ...]
sessions[--all]list of active sessions
my-browsers—schedules with Renter pivot
snapshot -o PATH{"chat", "screenshot": "PATH", "ts"}
screenshot -o PATH [--full]raw PNG/JPEG
navigate —{"ok": true}
click —{"ok": true}
type ""[--natural] [--no-human|--raw]{"ok": true}
scroll —{"ok": true}
switch-tab —closes previous, activates new
configure [--masking-mode true|false]{"ok": true}
cdp --method M [--params JSON]raw CDP response
wait —blocking: {"ended": true, "reason": "..."}
chat send ""—{"message_id": ...}
chat send-image--image PATH{"ok": true}
chat next--timeout=N (sec)next message or null
chat history[--since TS] [--limit N][ChatMessage, ...]
profile export-o PATHJSON with cookies+storage+fingerprint
profile import-i PATH{"ok": true}
upload --selector CSS --file PATH{"ok": true}
request-captcha [--manual]calls provider for manual solve
stop —{"ok": true}

Exit codes

CodeMeaning
0Success
1Generic error
2Auth (no CEKI_API_KEY)
3Session not found / expired
4Timeout
5Network / WS error

Errors go to stderr as JSON: {"error":"...","code":"..."}.

Lifecycle

rent ──► {session_id} ──► navigate/snapshot/click/type/... ──► stop

Each command is a separate subprocess. State between commands is held by resume: the relay keeps the session entry until explicit finish. After RELAY_RESUME_GRACE_MS=-1 the agent disconnect does NOT finish the session.

Session ends ONLY on: user_stop (provider pressed Stop), agent_end (you called ceki stop), provider_offline (browser lost network), insufficient_funds (balance ran out).

Stop is mandatory. Without it the meter keeps ticking.


Chat with the host

Each rental has a chat topic. Use it for human-in-the-loop testing: OTP/2FA codes from the host, CAPTCHA resolution for test environments, or confirmations before test payments.

# Send a message
ceki chat $SID send "Can you tell me the 6-digit code from your phone?"

# Wait for reply (up to 120s)
ANSWER=$(ceki chat $SID next --timeout=120 | jq -r .text)
[ "$ANSWER" = "null" ] && echo "no answer in time"

chat history vs chat next — two different operations

CommandDoesIdempotent?
chat historyReturns all past messages (optionally --since TS or --limit N). Does NOT mark anything as read.✅ Yes — call it repeatedly, same result
chat nextReturns the FIRST unread message, then advances last_seen_ts. If nothing unread, opens a WS subscription and waits up to --timeout seconds.❌ No — each call consumes the next message

Pattern: use history for catching up after a resume (no side effects). Use next for blocking wait on a new reply.


Rate limits — DON'T BE RUDE

Real-world incidents: agents hitting rate limits, then retrying in loops, making it worse. Follow these rules.

Rent rate limit: 20/hour per token

  • 20 ceki rent calls per hour per account. On the 21st → rate_limit error.
  • The counter resets on the UTC hour. Non-sliding window.
  • Each ceki stop + ceki rent cycle counts as 2 attempts. Plan long sessions instead of stop+rent per site.

Rate limit recovery — do NOT retry in loops

A tight polling loop on rent is the fastest way to block your token for 60+ minutes.

# When rent returns rate_limit:
# 1. STOP all retries immediately
# 2. Wait ≥10 minutes, or switch to a different token
# 3. Check remaining limit:
curl -s -H "Authorization: Bearer $CEKI_API_KEY" \
  https://api.ceki.me/api/browsers?limit=1 | jq '.meta'

Never write until ceki rent ... done loops. Each rate-limited attempt resets the monitoring window. A loop that hits rate_limit once will keep hitting it — the window only expands.

Other limits

  • CDP: 500 commands / 60s per session (command_rate_limit). ceki type --natural sends all keystrokes as ONE packet (plugin/413) — doesn't hit this limit.
  • Browser exclusivity: one active rental per browser_id at a time. "Browser is currently in use" means someone else has it — don't retry, pick another or wait.

Error handling — read stderr, check exit code

Distinguish real errors from "not available"

Symptomstderr / exit codeMeaning
rate_limit{"error":"rate_limit"}Token blocked for the hour window. Stop retries.
insufficient_fundsexit 1, stderr includes insufficientBalance at zero — top up the wallet. Not "busy", iterating other schedules won't help.
Browser is currently in use{"error":"busy"}That specific browser is rented by someone else. Try the next schedule_id.
no_providersexit 1All matching providers offline. Report, do NOT loop.
session_not_foundexit 3Session expired or was stopped. Can't resume — start fresh.
CDP no_sessionstderr no_sessionCDP channel only. Check native (ceki snapshot) — session likely alive.

Important patterns

Session persistence — export/import profiles

Each rental is a fresh incognito tab. To preserve login state across rents, export cookies + fingerprint:

ceki profile $SID export -o /tmp/profile.json
# Next rental:
SID=$(ceki rent --schedule $N --fingerprint-from /tmp/profile.json | jq -r .session_id)
ceki navigate $SID "https://site.com"
ceki profile import $SID -i /tmp/profile.json

Testing CAPTCHA behaviour

When testing your own sites that use CAPTCHA, be aware that a fresh incognito browser has no Google cookies — which can affect CAPTCHA scoring and the challenge type served. This is useful to understand when debugging CAPTCHA-related behaviour on your own site:

ceki navigate $SID https://www.youtube.com; sleep 7
ceki navigate $SID https://www.google.com; sleep 2
ceki click $SID 640 280
ceki type $SID "test query" --natural
# press Enter via CDP
ceki cdp $SID --method Input.dispatchKeyEvent --params '{"type":"keyDown","key":"Enter"}'
sleep 10
ceki navigate $SID https://target-site.com

This sequence builds Google cookie history so you can observe how CAPTCHA behaviour changes — useful for testing your own site's CAPTCHA configuration.


Behavioural profiles — profiles/

The skill ships with a library of behavioural profiles in profiles/. These are JSON descriptors that shape any input timing — typing speed, scroll rhythm, mouse trajectory, click delays — to simulate different user demographics and platform-specific interaction patterns. Use them for realistic QA testing across a range of user personas.

Demographic profiles

Each profile represents a user demographic with distinct interaction characteristics. Pass the filename (without .json) as a human preset to the SDK.

ProfileTyping speedBehaviour
tech-worker-25-40100-145 wpmFast, minimal scroll, low think pauses
executive-35-5570-110 wpmDeliberate, moderate scroll, formal
college-male-18-2490-130 wpmErratic hours, fast scroll, high backspace
college-female-18-2485-125 wpmNight-owl, social-heavy

And 13 more: creative-professional, freelancer, gamer, middle-aged-male/female, night-shift-worker, parent, rural-user, senior-male/female, social-media-power-user, student-highschool, teen-boys/girls, urban-professional-male/female.

from ceki_sdk import Browser, HumanProfile

# Load a preset
profile = HumanProfile.load_preset("tech-worker-25-40")

async with Browser(token="...", human=profile) as br:
    await br.session(mode="incognito")

Domain profiles

Each domain profile encodes the interaction patterns of a specific platform — scroll depth, reading pauses, click targets, login flow, and CSS selectors.

ProfileDomainKey behaviour
domain-twittertwitter.com, x.comFast timeline scroll, tweet-level pauses, like/reply patterns
domain-linkedinlinkedin.comSlow professional scroll, comment typing, connection clicks
domain-redditreddit.comThread scanning, expand-collapse, vote patterns
domain-youtubeyoutube.comVideo browsing, search, comment section behaviour
domain-amazonamazon.comProduct search, listing scroll, review reading
domain-facebookfacebook.comNews feed scroll, reaction clicks, comment typing
domain-instagraminstagram.comImage-first scroll, story viewing, like patterns
domain-tiktoktiktok.comVideo-first scroll, short attention, fast swipe

Each domain profile includes platform_specific data:

{
  "requires_login": true,
  "login": { "pre_login_pause_ms": 1500, ... },
  "selectors_css": {
    "like_button": "[data-testid='like']",
    "tweet_cell": "[data-testid='cellInnerDiv']"
  },
  "sequence_hint": "navigate timeline → scroll 3-5 passes → pause → like | reply → scroll more"
}

Use these as reference when scripting for a specific platform — the selectors and sequence hints give you the DOM targets and interaction order without reverse-engineering each site.

These are reference profiles for scripting platform-specific interactions — the selectors and sequence hints give you the DOM targets and interaction order without reverse-engineering each site.

Pacing profiles (extension-side)

Separate from the human profiles above, pacing profiles control the post-navigation pause the browser extension inserts before the first interaction — critical for realistic user simulation in QA testing.

ProfileRead delayScroll segmentsPre-clickWhen to use
minimal200-800ms0200-600msSpeed-critical, low-sensitivity sites
normal (default)1.5-4s2200-600msGeneral browsing
aggressive3-7s3300-1000msHigh-sensitivity (banks, Google, Cloudflare)
random———Random behaviour, no fixed pattern

Set via SDK:

async with Browser(token="...") as br:
    async with await br.session(
        mode="incognito",
        pacing_profile="aggressive"  # ← extension adds 3-7s read before first action
    ) as s:
        ...

Browser search data

When you ceki search, each result includes rich metadata for picking the right browser:

{
  "schedule_id": 12345,
  "geo": "US",
  "language": "en",
  "rating": 4.8,
  "price_per_min": 0.03,
  "domain_allowed": ["*"],
  "allowed_domains": null,
  "online": true,
  "skills": ["scraping", "captcha"]
}

Filter with --filter:

ceki search --limit 10 --filter geo=US --filter price_per_min<=0.05

Available filters: geo, language, price_per_min, rating, online, profile_mode, allowed_domains.


Environments — dev vs prod

Dev (ittribe)Prod (default)
CEKI_API_URLhttps://clawapi.ittribe.orgomit → api.ceki.me
CEKI_RELAY_URLwss://browser.ittribe.org/ws/agentomit → browser.ceki.me
CEKI_CHAT_URLhttps://chat.ittribe.org/api/chatomit → chat.ceki.me

Session modes

ModeDescription
incognitoClean browser, no user cookies. Best for anonymous scraping.
main / personaReal user cookies and profile of the host. Password fields protected.

Detailed reference

  • reference/methods.md — full JSON-RPC method reference
  • reference/pricing.md — pricing details
  • examples/ — quickstarts (Python, TypeScript, IDE configs) and full examples

Related skills

Join a video meeting as an AI bot with voice, avatar, and screenshare across four operating modes.

by johnpatternai22 installs8 stars

Generate and edit Draw.io, Mermaid, and Excalidraw diagrams from natural language using a structured JSON spec.

by nssa.io1.0k installs47 stars

Stores durable facts in a categorized, plain-markdown vault on disk, alongside your agent's built-in memory.

by Iván559 installs18 stars

Find why your productivity system keeps failing, then apply the smallest fix — capacity math, bottleneck routing, durable local notes.

by Iván855 installs69 stars

Fetch raw ad creative, app, ranking, and revenue data from AdMapix as structured JSON.

by fly0pants4.3k installs296 stars

More from iwedmak

Browse all skills

Lets your AI agent drive a real Chrome session (yours or a marketplace participant's) for tasks where headless or scripted requests are not enough — your own...

by iwedmak2 installs