原始内容
name: real-browser-qa-ceki
description: QA and security testing via real Chrome browser sessions — realistic user simulation, vulnerability discovery, cross-browser inconsistency detection. Pre-flight: sessions → my-browsers → search. Type with --natural, probe with snapshot, distinguish insufficient_funds vs busy. Generic marketplace + env-driven self mode.
when_to_use: An agent needs a real Chrome session for QA testing, security auditing, or vulnerability discovery — realistic user simulation, form flow testing, CAPTCHA/2FA flow testing, cross-browser inconsistency detection, and any scenario where headless mode hides real-world issues.
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_tabcloses 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 eval |
ceki scroll → ceki snapshot → read the page |
| GraphQL/API calls to internal endpoints | Fill 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:
- A realistic pause before it (user looks at the screen, moves the mouse, thinks)
- Natural trajectory (mouse doesn't teleport; cursor moves along a path)
- Logical sequence (user fills top-to-bottom, not random order)
Realistic vs unrealistic interaction table
| Action | ❌ Unrealistic (hides bugs) | ✅ Realistic (reveals issues) |
|---|---|---|
| Page load | Starts clicking immediately | Pause 800-2500ms — tests lazy-load, above-fold rendering |
| Button click | Mouse teleports instantly | Cursor follows curve, 200-600ms hover before click — tests hover states, tooltips |
| Form fill | All fields in 1-2 seconds | Field by field, 300-900ms between fields, scroll between sections — tests real UX flow |
| Text input | Text appears instantly | ceki type --natural — keystrokes with pauses, 80-110 WPM, occasional typos — tests input validation, character limits |
| Scroll | scrollTo(0, documentHeight) — jump to bottom |
2-3 passes, pause 400-1500ms between passes — tests infinite scroll, lazy images, sticky headers |
| Checkbox / radio | Clicks without pause, dead center | Slight offset, 300-700ms aiming — tests click area, label association |
| Input error | Never makes mistakes | ~3% keystrokes — typo → backspace → correction — tests validation UX, error recovery |
| Select / dropdown | Picks first option instantly | Scrolls list, selects unhurriedly, 500-1500ms — tests dropdown rendering, overflow |
| Multitasking | Click-scroll-type in parallel | One action at a time. Tests real user flow timing |
| Pacing | Always identical | Varies 2-3× between actions (normal: 300-3000ms) — tests against timing-based bugs |
What this enables for QA
| Test type | How realistic simulation helps |
|---|---|
| Layout & rendering | Real scroll patterns reveal sticky header glitches, lazy-load timing, CLS issues |
| Form validation | Natural typing speed triggers async validation, character counters, debounced checks |
| Auth / CAPTCHA flows | Real fingerprint avoids false positives; delegate to human for CAPTCHA solve in test env |
| Cross-browser testing | Same test across different geo/IP profiles catches geo-specific behaviour |
| Payment flows | Realistic fill + human-in-the-loop for 2FA/OTP reveals payment UX bugs |
| Vulnerability discovery | Real browser fingerprint bypasses naive WAF rules; tests against real rendering engine |
| Inconsistency detection | Same 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 parameter | What it controls | Urban Professional | College Student |
|---|---|---|---|
typing.wpm |
Typing speed | 80-110 (fast) | 50-80 (slower) |
typing.backspace_freq |
Typo frequency | 3% | 7% |
typing.think_ms |
Think pause | 300-1000ms | 500-2000ms |
scroll.speed_px_per_ms |
Scroll speed | 25-60 px/ms | 15-40 px/ms |
scroll.pause_between_passes |
Pause between passes | 400-1500ms | 600-2500ms |
mouse.curvature |
Cursor curve | 0.15 (confident) | 0.35 (less precise) |
mouse.jitter |
Mouse jitter | 0.3 | 0.5 |
navigation.pre_click_pause |
Pre-click delay | 200-600ms | 300-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
| Pattern | Why it matters for QA |
|---|---|
| Zero pause between actions | Real users can't click faster than 100ms — tests that miss realistic pacing miss timing bugs |
| Parallel actions | Real users don't scroll and type simultaneously — tests should match real use |
| Mouse teleport | Without intermediate points, hover-dependent UI (tooltips, dropdowns) never triggers |
| Bottom-up fill order | Real users tab through fields top-to-bottom — tests bottom-up miss tab-order bugs |
| Perfect input | Real users make typos — tests without typos miss error-recovery UX |
| Identical timing | Sets of identical delays are unnatural — tests should randomize to surface timing races |
| Instant submit after page load | Real users read before submitting — tests that skip this miss pre-submit validation states |
| Click at pixel (0,0) or screen edge | Real 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: <what you're doing>" \
--status 100 --type 2 \
--benefitable agent:N --desc "Task: <description>
Plan:
1. Rent browser
2. Navigate to target
3. ..."
# Note the returned event_id (eid), then:
ceki timelog start <eid>
When the session completes:
ceki contract progress <eid> --status 222 --desc "Done: <result>"
ceki timelog stop <eid> --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.txtand 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 mode | The host sees | You should know |
|---|---|---|
| Self (your browser) | Nothing — it's your own machine | Standard browser visibility |
| Marketplace (renting) | Everything your agent does — screen, navigation, keystrokes, chat | Do 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/importlocally.
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: <brief> " --status 100 --type 2 \
--benefitable agent:N --desc "Plan: 1) Rent 2) Navigate 3) ..."
ceki timelog start <eid>
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 <session_id>
# If none → proceed to step 2
A relay session lives forever (
RELAY_RESUME_GRACE_MS=-1) until you callceki stop. Returning after a pause? Your session is still there —ceki sessionsfinds it,ceki rent --resumereconnects. 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:
| Mode | What it does | Cost | Setup |
|---|---|---|---|
| Self | Your own Chrome via Ceki extension. Free when host == renter. | Free | Install extension from browser.ceki.me/install |
| Marketplace | Rent a Chrome from an opted-in host. You see their browser; they see your session. | ~$0.03/min, USDC | ceki search → ceki rent --schedule <id> |
| Earn (opt-in) | Share your idle Chrome so other agents can rent it. Off by default. | You receive 90% of session price | Toggle 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. | Varies | CEKI_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 var | Purpose | Default |
|---|---|---|
CEKI_RENT_SCHEDULES |
Comma-separated schedule_ids, priority order | empty → fallback to ceki search |
CEKI_RENT_MODE |
main or incognito |
main |
CEKI_TOKEN / CEKI_API_KEY |
Sanctum token (ag_*) |
— |
CEKI_API_URL |
API base URL | https://api.ceki.me |
CEKI_RELAY_URL |
WebSocket relay | wss://browser.ceki.me/ws/agent |
CEKI_CHAT_URL |
Chat service | https://chat.ceki.me/api/chat |
Marketplace (generic)
- Register at ceki.me
- Create a Sanctum token: dashboard → Profile → API Keys
- Pass as
token=<sanctum_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 <sid> "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
| Flag | Effect | When to use |
|---|---|---|
--natural (recommended) |
Human-like typing with pauses between keystrokes, realistic interaction pattern | Every 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 / --raw |
Sends text instantly as real key events but no pauses | Bulk data entry, pasting long text, hidden fields |
Why type --natural beats CDP value-setter
ceki type(any flag) sends realkeydown/keypress/keyupevents. React fires_valueTracker, Vue catchesinputevent — 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 missesv-model→ form submit silently fails with "required".--naturaladds 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,\"<TEXT>\");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 <input>/<textarea> — 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,\"<TEXT>\");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 <input>/<textarea> — 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.
| Channel | Commands | Session health indicator |
|---|---|---|
| Native | rent, navigate, snapshot, click, type, sessions, my-browsers, stop |
✅ Source of truth |
| CDP | ceki 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
| cmd | args | stdout 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 <sid> |
-o PATH |
{"chat", "screenshot": "PATH", "ts"} |
screenshot <sid> |
-o PATH [--full] |
raw PNG/JPEG |
navigate <sid> <url> |
— | {"ok": true} |
click <sid> <x> <y> |
— | {"ok": true} |
type <sid> "<text>" |
[--natural] [--no-human|--raw] |
{"ok": true} |
scroll <sid> <x> <y> <dy> |
— | {"ok": true} |
switch-tab <sid> |
— | closes previous, activates new |
configure <sid> |
[--masking-mode true|false] |
{"ok": true} |
cdp <sid> |
--method M [--params JSON] |
raw CDP response |
wait <sid> |
— | blocking: {"ended": true, "reason": "..."} |
chat <sid> send "<text>" |
— | {"message_id": ...} |
chat <sid> send-image |
--image PATH |
{"ok": true} |
chat <sid> next |
--timeout=N (sec) |
next message or null |
chat <sid> history |
[--since TS] [--limit N] |
[ChatMessage, ...] |
profile <sid> export |
-o PATH |
JSON with cookies+storage+fingerprint |
profile <sid> import |
-i PATH |
{"ok": true} |
upload <sid> |
--selector CSS --file PATH |
{"ok": true} |
request-captcha <sid> |
[--manual] |
calls provider for manual solve |
stop <sid> |
— | {"ok": true} |
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic error |
| 2 | Auth (no CEKI_API_KEY) |
| 3 | Session not found / expired |
| 4 | Timeout |
| 5 | Network / 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
| Command | Does | Idempotent? |
|---|---|---|
chat history |
Returns all past messages (optionally --since TS or --limit N). Does NOT mark anything as read. |
✅ Yes — call it repeatedly, same result |
chat next |
Returns 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 rentcalls per hour per account. On the 21st →rate_limiterror. - The counter resets on the UTC hour. Non-sliding window.
- Each
ceki stop+ceki rentcycle 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 --naturalsends 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"
| Symptom | stderr / exit code | Meaning |
|---|---|---|
rate_limit |
{"error":"rate_limit"} |
Token blocked for the hour window. Stop retries. |
insufficient_funds |
exit 1, stderr includes insufficient |
Balance 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_providers |
exit 1 | All matching providers offline. Report, do NOT loop. |
session_not_found |
exit 3 | Session expired or was stopped. Can't resume — start fresh. |
CDP no_session |
stderr no_session |
CDP 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.
| Profile | Typing speed | Behaviour |
|---|---|---|
tech-worker-25-40 |
100-145 wpm | Fast, minimal scroll, low think pauses |
executive-35-55 |
70-110 wpm | Deliberate, moderate scroll, formal |
college-male-18-24 |
90-130 wpm | Erratic hours, fast scroll, high backspace |
college-female-18-24 |
85-125 wpm | Night-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.
| Profile | Domain | Key behaviour |
|---|---|---|
domain-twitter |
twitter.com, x.com | Fast timeline scroll, tweet-level pauses, like/reply patterns |
domain-linkedin |
linkedin.com | Slow professional scroll, comment typing, connection clicks |
domain-reddit |
reddit.com | Thread scanning, expand-collapse, vote patterns |
domain-youtube |
youtube.com | Video browsing, search, comment section behaviour |
domain-amazon |
amazon.com | Product search, listing scroll, review reading |
domain-facebook |
facebook.com | News feed scroll, reaction clicks, comment typing |
domain-instagram |
instagram.com | Image-first scroll, story viewing, like patterns |
domain-tiktok |
tiktok.com | Video-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.
| Profile | Read delay | Scroll segments | Pre-click | When to use |
|---|---|---|---|---|
minimal |
200-800ms | 0 | 200-600ms | Speed-critical, low-sensitivity sites |
normal (default) |
1.5-4s | 2 | 200-600ms | General browsing |
aggressive |
3-7s | 3 | 300-1000ms | High-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_URL |
https://clawapi.ittribe.org |
omit → api.ceki.me |
CEKI_RELAY_URL |
wss://browser.ittribe.org/ws/agent |
omit → browser.ceki.me |
CEKI_CHAT_URL |
https://chat.ittribe.org/api/chat |
omit → chat.ceki.me |
Session modes
| Mode | Description |
|---|---|
incognito |
Clean browser, no user cookies. Best for anonymous scraping. |
main / persona |
Real user cookies and profile of the host. Password fields protected. |
Detailed reference
reference/methods.md— full JSON-RPC method referencereference/pricing.md— pricing detailsexamples/— quickstarts (Python, TypeScript, IDE configs) and full examples