OpenClaw already gives you three useful primitives:
web_fetchfor plain HTTP fetch + readable extractionbrowserfor local or remote CDP-driven browser control- skills for teaching the agent new tool flows
browser.city fits best as the stealth browser backend when you need:
- JS rendering + clean markdown on blocked sites
- residential / geo egress
- remote interactive steps over HTTP (
/v1/do/*) - extraction as an API instead of driving a local browser profile
Practical split:
- Use OpenClaw’s built-in
web_fetchfor simple public pages. - Use OpenClaw’s
browsertool for local/manual-login flows and human verification. - Use browser.city when the page is guarded, JS-heavy, geo-sensitive, or you want deterministic remote browser infrastructure behind a skill.
1) Add a browser.city skill to OpenClaw
OpenClaw skills can live in either:
<workspace>/skills/browsercity~/.openclaw/workspace/skills/browsercity
In SKILL.md, tell OpenClaw to:
- prefer browser.city Request API for
URL -> markdown - escalate to browser.city Humanized REST (
/v1/do/*) when a page needs click/type/navigation - keep secrets in
BROWSERCITY_API_KEY - fall back to the native OpenClaw browser tool when you explicitly want a local/manual-login browser
You do not need a heavy plugin for this. A simple skill that points the agent at one of the helpers below is enough.
2) Helper: URL -> markdown (Request API)
This is the best default for OpenClaw when you want a deterministic read tool with stealth rendering.
export async function browsercityMarkdown(url: string): Promise<string> { const res = await fetch("https://api.browser.city/v1/requests", { method: "POST", headers: { Authorization: `Bearer ${process.env.BROWSERCITY_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ url, markdown: true }), }).then((r) => r.json()); return String(res.content ?? "");}import osimport requestsapi_key = os.environ["BROWSERCITY_API_KEY"]def browsercity_markdown(url: str) -> str: res = requests.post( "https://api.browser.city/v1/requests", headers={"Authorization": f"Bearer {api_key}"}, json={"url": url, "markdown": True}, ).json() return str(res.get("content", ""))using System.Net.Http.Headers;using System.Net.Http.Json;var http = new HttpClient();http.DefaultRequestHeaders.Authorization = new( "Bearer", Environment.GetEnvironmentVariable("BROWSERCITY_API_KEY"));async Task<string> BrowsercityMarkdown(string url) { var res = await (await http.PostAsJsonAsync( "https://api.browser.city/v1/requests", new { url, markdown = true })) .Content.ReadFromJsonAsync<Response>(); return res?.Content ?? "";}record Response(string? Content);import com.fasterxml.jackson.databind.ObjectMapper;import java.net.URI;import java.net.http.*;import java.util.Map;public class BrowsercityRequest { static final ObjectMapper JSON = new ObjectMapper(); static String browsercityMarkdown(String url) throws Exception { var body = JSON.writeValueAsString(Map.of("url", url, "markdown", true)); var req = HttpRequest.newBuilder(URI.create("https://api.browser.city/v1/requests")) .header("Authorization", "Bearer %s".formatted(System.getenv("BROWSERCITY_API_KEY"))) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(body)).build(); var res = JSON.readValue( HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString()).body(), Response.class); return res.content() == null ? "" : res.content(); } record Response(String content) {}}
3) Helper: open -> navigate -> markdown (Humanized REST)
When OpenClaw needs a remote browser session but you do not want to run Playwright inside the OpenClaw runtime, use /v1/do/*.
const doUrl = (action: string) => `https://api.browser.city/v1/do/${action}`;const doAction = (action: string, body: unknown) => fetch(doUrl(action), { method: 'POST', headers: { Authorization: `Bearer ${process.env.BROWSERCITY_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }).then((r) => r.json());export async function browsercityBrowse(url: string) { const opened = await doAction('open', { browser: 'chromium' }); try { await doAction('navigate', { sessionId: opened.sessionId, url }); const page = await doAction('markdown', { sessionId: opened.sessionId }); return page.result; } finally { await doAction('close', { sessionId: opened.sessionId }); }}using System.Net.Http.Headers;using System.Net.Http.Json;using System.Text.Json.Nodes;var http = new HttpClient();http.DefaultRequestHeaders.Authorization = new( "Bearer", Environment.GetEnvironmentVariable("BROWSERCITY_API_KEY"));async Task<JsonNode?> Do(string action, object body) => await (await http.PostAsJsonAsync( new Uri($"https://api.browser.city/v1/do/{action}"), body)).Content.ReadFromJsonAsync<JsonNode>();async Task<string?> Browse(string url) { var opened = await Do("open", new { browser = "chromium" }); try { await Do("navigate", new { sessionId = (string?)opened!["sessionId"], url }); var page = await Do("markdown", new { sessionId = (string?)opened!["sessionId"] }); return (string?)page!["result"]; } finally { await Do("close", new { sessionId = (string?)opened!["sessionId"] }); }}import com.fasterxml.jackson.databind.*;import java.net.URI;import java.net.http.*;import java.util.Map;public class BrowsercityBrowse { static final String DO_API_BASE = "https://api.browser.city/v1/do"; static final String API_KEY = System.getenv("BROWSERCITY_API_KEY"); static final HttpClient HTTP = HttpClient.newHttpClient(); static final ObjectMapper JSON = new ObjectMapper(); static String browse(String url) throws Exception { var opened = callAction("open", Map.of("browser", "chromium")); var sessionId = opened.get("sessionId").asText(); try { callAction("navigate", Map.of("sessionId", sessionId, "url", url)); return callAction("markdown", Map.of("sessionId", sessionId)).get("result").asText(); } finally { callAction("close", Map.of("sessionId", sessionId)); } } static JsonNode callAction(String action, Map<String, ?> body) throws Exception { var req = HttpRequest.newBuilder(URI.create("%s/%s".formatted(DO_API_BASE, action))) .header("Authorization", "Bearer %s".formatted(API_KEY)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(JSON.writeValueAsString(body))).build(); return JSON.readTree(HTTP.send(req, HttpResponse.BodyHandlers.ofString()).body()); }}
4) Good OpenClaw prompt patterns
Try prompts like:
Use the browsercity skill to fetch
https://example.com/docsas markdown. If the page needs interaction, escalate to the interactive helper.
Use the native OpenClaw browser for manual login, then switch back to browser.city for large-scale extraction.
That split works well because OpenClaw remains the planner, while browser.city handles the stealth browsing primitives.
What to use when
- Use OpenClaw
web_fetchfor fast public pages with no JS requirements. - Use OpenClaw
browserfor local/manual-login browsing and human verification. - Use browser.city Request API for stealth
URL -> markdownat scale. - Use browser.city Humanized REST for remote interactive flows without Playwright inside OpenClaw.
- Use browser.city Sessions only when you already want a real Playwright workflow outside OpenClaw.