PowerScan

Automating Browser Profiles with a Local API

Automation · 8 min read · by PowerOps

Photo: Unsplash

Start a profile from a script, get back a debugging endpoint, and attach Playwright or Puppeteer to a browser that already has its identity and proxy attached.

Automating an antidetect browser is usually presented as a hard problem. It is not, provided the tool hands you the one thing your automation library already knows how to use: a debugging endpoint.

The shape of it

PowerOps runs an HTTP API bound to the loopback address. Your script asks it to start a profile; it launches the browser with that profile's identity, storage and proxy already attached, and returns the WebSocket endpoint of the running browser. Your script connects to that endpoint with Playwright, Puppeteer or anything else that speaks the DevTools protocol.

The automation library never has to know what a fingerprint is. By the time it connects, the environment is already established.

Starting a profile

curl -s -H "Authorization: Bearer $POWEROPS_API_KEY" \
  http://127.0.0.1:50326/status

Every route except the status check requires a bearer key. Starting a browser returns the endpoint to attach to:

curl -s -X POST \
  -H "Authorization: Bearer $POWEROPS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"profileId": "..."}' \
  http://127.0.0.1:50326/browser/start

Attaching your library

import { chromium } from "playwright";

const res = await fetch("http://127.0.0.1:50326/browser/start", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.POWEROPS_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ profileId }),
});

const { ws } = await res.json();

// The profile's identity, storage and proxy are already in place.
const browser = await chromium.connectOverCDP(ws);
const [context] = browser.contexts();
const page = context.pages()[0] ?? await context.newPage();

await page.goto("https://example.com");

The important line is the comment. There is no fingerprint configuration in your script, because configuring the environment is not your script's job. It belongs to the profile, and it stays the same whether the profile is opened by a person clicking a row or by a scheduled job at three in the morning.

What the API covers

How it is kept local

An API that drives browsers holding signed-in accounts deserves a word about its boundaries.

A pattern worth copying

Query the profiles in a group, start each one, do the work, stop it. Run them a few at a time rather than all at once. The constraint is memory on your own machine, since each profile is a real browser process.

const { profiles } = await api("/profiles?groupId=" + groupId);

for (const batch of chunk(profiles, 5)) {
  await Promise.all(batch.map(async (profile) => {
    const { ws } = await api("/browser/start", { profileId: profile.id });
    try {
      await doTheWork(ws);
    } finally {
      await api("/browser/stop", { profileId: profile.id });
    }
  }));
}

Stopping the profile in a `finally` matters more than it looks: closing cleanly is what writes the browser state out, and a profile killed mid-session can lose the session it just established.

All articles · Run the scan