← back to blog

Anti-detect browser API automation: comparing Multilogin, AdsPower, GoLogin

Anti-detect browser API automation: comparing Multilogin, AdsPower, GoLogin

manually clicking through 50 browser profiles is how you start. it is not how you stay in business. once you hit double-digit profile counts, manual operation becomes the actual bottleneck: you spend more time opening and closing browsers than doing the work those browsers exist to do. the solution is API automation, where your code starts profiles, drives sessions, and closes them without you touching a mouse. every major anti-detect browser ships a local HTTP API for exactly this. what they do not do is make the differences obvious.

i have run all three of these platforms in production, primarily for affiliate traffic arbitrage and airdrop farming. the surface-level pitch is the same across vendors: expose a localhost endpoint, return a Chrome DevTools Protocol (CDP) debug URL, let you attach Puppeteer or Playwright. the reality is that the APIs differ in authentication model, error verbosity, profile state handling, and how they behave when something goes wrong at 3am with no one watching. those differences matter more than the marketing copy.

this piece is for people who have already read the reviews, picked a tool, and now want to actually automate it at scale. i will skip the fingerprint theory and go straight to API structure, connection patterns, concrete code, and the failure modes i have personally stepped on.

background and prior art

the underlying technology that makes this possible is not new. the Chrome DevTools Protocol has been publicly documented since 2017 and is the same interface used by Chrome’s built-in DevTools, Lighthouse, and every serious browser automation framework. when you open a Chromium instance with the --remote-debugging-port flag, it exposes a WebSocket endpoint you can attach to from any language. Puppeteer and Playwright both consume this protocol natively. Selenium, as of the W3C WebDriver specification, uses a different but compatible wire protocol that most anti-detect browsers also support via a bundled ChromeDriver.

anti-detect browsers are, at their core, patched Chromium builds (or Firefox for Stealthfox/FlowerBrowser variants) that intercept and spoof the fingerprint APIs before any JavaScript can read them. the fingerprint profiles live in vendor-controlled storage, not in the browser’s user data directory. what the local APIs add on top of CDP is the ability to say “start profile X with these proxy credentials and return me the debug port” without you having to manage the underlying browser binary, user data path, or environment variables directly.

prior to dedicated anti-detect APIs, people ran bare Chromium with custom launch scripts, patching flags manually. it worked but it was brittle: fingerprint consistency was poor, updating the browser could break patches, and you had no central store for cookies or profile metadata. the current generation of vendor APIs abstracts all of that. the tradeoff is vendor lock-in and the local API being a single point of failure for your entire automation stack.

the core mechanism

all three platforms follow the same pattern, which i will describe before getting into the differences.

  1. you start a browser profile via a local HTTP API call (usually GET or POST to http://localhost:PORT/api/...)
  2. the API response contains a CDP debug URL (websocket) and/or a ChromeDriver endpoint
  3. you pass that URL to Puppeteer’s connect(), Playwright’s connect(), or Selenium’s Remote() constructor
  4. from that point your automation code drives the browser exactly as it would any other Chromium instance
  5. when you are done, you call a stop endpoint to close the browser and flush cookies back to the vendor’s storage

the key insight: your Puppeteer or Playwright code does not need to know it is talking to an anti-detect browser. from the automation framework’s perspective it is just a remote CDP endpoint. this is architecturally clean and means you can swap vendors by changing only the connection setup, not your page interaction logic.

Multilogin X

Multilogin runs a local agent that you install separately from the main app. the agent listens on port 35000 by default. as of early 2025, Multilogin X uses a token-based authentication model: you obtain a token via a POST to /user/signin with your account credentials, then pass that token in the Authorization header for all subsequent calls.

starting a profile:

GET http://localhost:35000/api/v1/profile/start?profileId=YOUR_PROFILE_ID
Authorization: Bearer YOUR_TOKEN

the response includes a value field with the ChromeDriver URL:

{
  "status": "OK",
  "value": "http://localhost:PORT"
}

you then point Selenium’s Remote driver at that URL, or extract the CDP WS debugger URL from http://localhost:PORT/json/version and pass it to Puppeteer’s [puppeteer](https://pptr.dev/).connect({ browserWSEndpoint }).

one thing Multilogin does better than the others: the agent exposes profile state, so you can query whether a profile is already running before trying to start it. this prevents the “profile already open” error that will silently corrupt a session if you are not handling it.

AdsPower

AdsPower’s local API runs on port 50325 and resolves via http://local.[adspower](https://www.adspower.com/).net:50325 (they set up a local DNS entry during install, which is a nice touch for code readability). the API does not require authentication tokens for local calls by default, which is simpler but means anything running on the machine can hit it.

starting a browser:

GET http://local.adspower.net:50325/api/v1/browser/start?user_id=YOUR_PROFILE_ID

the response is verbose and contains both a Selenium WebDriver URL and a CDP WebSocket URL:

{
  "code": 0,
  "data": {
    "ws": {
      "puppeteer": "ws://localhost:PORT/devtools/browser/GUID",
      "selenium": "http://localhost:PORT"
    },
    "webdriver": "/path/to/chromedriver"
  }
}

this is the most complete response of the three. you get both endpoints in one call, which saves a round-trip. AdsPower also returns the path to its bundled ChromeDriver binary, which matters if you are using Selenium and need to match driver versions.

connecting with Puppeteer:

const puppeteer = require('puppeteer-core');

async function connectToAdsPower(profileId) {
  const res = await fetch(
    `http://local.adspower.net:50325/api/v1/browser/start?user_id=${profileId}`
  );
  const data = await res.json();

  if (data.code !== 0) throw new Error(`AdsPower error: ${data.msg}`);

  const browser = await puppeteer.connect({
    browserWSEndpoint: data.data.ws.puppeteer,
    defaultViewport: null
  });

  return browser;
}

GoLogin

GoLogin’s local API runs on port 36912. like AdsPower, no auth token is required for local calls. the start endpoint is slightly different: you POST a profile ID and receive a debug port number, not a full URL.

POST http://localhost:36912/browser/start
Content-Type: application/json

{ "profileId": "YOUR_PROFILE_ID" }

response:

{
  "status": "success",
  "wsUrl": "ws://localhost:PORT/devtools/browser/GUID"
}

GoLogin also ships an official Node.js SDK that wraps these calls, which is useful for getting started but adds a dependency you may want to avoid in production. the raw API is straightforward enough that the SDK is optional. GoLogin additionally offers a cloud API (against their hosted service, not a local agent), which is useful if you want to start profiles from a remote server rather than running the GUI app locally. that distinction matters for server-side deployments, covered more in the production notes below.

worked examples

example 1: cycling 20 AdsPower profiles for a daily check-in task

this is a common airdrop farming pattern. you have 20 profiles, each tied to a different wallet and set of cookies. you want to run through all of them each morning, perform a login-gated action, and close each browser before opening the next.

const puppeteer = require('puppeteer-core');
const fetch = require('node-fetch');

const BASE = 'http://local.adspower.net:50325';
const PROFILE_IDS = ['abc123', 'def456', /* ... 18 more */];
const DELAY_MS = 8000; // 8 second pause between profiles

async function runProfile(profileId) {
  // start browser
  const startRes = await fetch(`${BASE}/api/v1/browser/start?user_id=${profileId}`);
  const startData = await startRes.json();
  if (startData.code !== 0) throw new Error(startData.msg);

  const browser = await puppeteer.connect({
    browserWSEndpoint: startData.data.ws.puppeteer,
    defaultViewport: null
  });

  const [page] = await browser.pages();

  try {
    await page.goto('https://target-dapp.example.com/checkin', {
      waitUntil: 'networkidle2',
      timeout: 30000
    });
    // do work here
    await page.click('#claim-button');
    await page.waitForTimeout(2000);
  } finally {
    await browser.disconnect(); // disconnect, don't close
    await fetch(`${BASE}/api/v1/browser/stop?user_id=${profileId}`);
  }
}

(async () => {
  for (const id of PROFILE_IDS) {
    try {
      await runProfile(id);
      console.log(`done: ${id}`);
    } catch (e) {
      console.error(`failed: ${id}`, e.message);
    }
    await new Promise(r => setTimeout(r, DELAY_MS));
  }
})();

note the browser.disconnect() instead of browser.close(). closing via Puppeteer terminates the browser process immediately, bypassing AdsPower’s cookie sync. disconnecting first, then calling the stop API, gives the vendor client time to flush session state.

example 2: Multilogin X with parallel profile execution

for higher throughput you might want to run 5 profiles concurrently. Multilogin’s plan limits apply here: the €99/month Solo plan allows 100 profiles but limits how many can run simultaneously depending on your subscription tier. always check your plan’s concurrent session limit before writing parallel code or you will hit 429s.

import asyncio
import aiohttp
from pyppeteer import connect

AGENT_BASE = "http://localhost:35000/api/v1"
TOKEN = "YOUR_MULTILOGIN_TOKEN"
PROFILE_IDS = ["id1", "id2", "id3", "id4", "id5"]

async def run_profile(session, profile_id):
    headers = {"Authorization": f"Bearer {TOKEN}"}

    async with session.get(
        f"{AGENT_BASE}/profile/start",
        params={"profileId": profile_id},
        headers=headers
    ) as resp:
        data = await resp.json()
        if data["status"] != "OK":
            raise Exception(f"start failed: {data}")
        driver_url = data["value"]

    # get CDP WS endpoint
    async with session.get(f"{driver_url}/json/version") as resp:
        version_data = await resp.json()
        ws_url = version_data["webSocketDebuggerUrl"]

    browser = await connect(browserWSEndpoint=ws_url)
    page = await browser.newPage()

    try:
        await page.goto("https://target.example.com")
        # task logic here
    finally:
        await browser.disconnect()
        async with session.get(
            f"{AGENT_BASE}/profile/stop",
            params={"profileId": profile_id},
            headers=headers
        ) as _:
            pass

async def main():
    async with aiohttp.ClientSession() as session:
        await asyncio.gather(*[run_profile(session, pid) for pid in PROFILE_IDS])

asyncio.run(main())

five concurrent sessions is usually stable on a mid-range workstation. more than that and you start hitting RAM limits at around 800MB per profile depending on what the page loads.

example 3: GoLogin profile rotation with proxy swap

GoLogin’s API lets you update a profile’s proxy before starting it, which is useful if you are rotating residential proxies from an external pool rather than hardcoding them into each profile. for more on proxy pooling strategies, proxyscraping.org has a solid breakdown of residential vs datacenter rotation that is worth reading alongside this.

const fetch = require('node-fetch');

const GL_BASE = 'http://localhost:36912';

async function startWithProxy(profileId, proxyConfig) {
  // update proxy before start
  await fetch(`${GL_BASE}/browser/${profileId}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      proxy: {
        mode: 'http',
        host: proxyConfig.host,
        port: proxyConfig.port,
        username: proxyConfig.user,
        password: proxyConfig.pass
      }
    })
  });

  // start browser
  const startRes = await fetch(`${GL_BASE}/browser/start`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ profileId })
  });

  const { wsUrl } = await startRes.json();
  return wsUrl;
}

this pattern lets you keep profiles fingerprint-stable while rotating IPs, which is important for platforms that correlate IP changes with behavioral anomalies. see also the multiaccountops.com blog for discussion on IP-to-profile assignment strategies in multi-account workflows.

edge cases and failure modes

1. the “already running” race condition

if your script crashes mid-session, the profile stays open in the vendor’s process table but your code has no reference to it. the next run tries to start the same profile and gets an error (Multilogin: “profile is already running”, AdsPower: code: -1). fix this by calling the stop endpoint at script start for any profile you are about to use, catching and ignoring the error if it was not running. do not try to reconnect to an orphaned session unless you are storing the WS URL persistently, which introduces its own complexity.

2. CDP connection timing

the start API returns before the browser is fully ready to accept CDP connections. Multilogin’s response is usually safe to connect to immediately. AdsPower occasionally returns the WS URL 200-500ms before the browser has opened the port. the fix is a retry loop on the WebSocket connection with a 100ms backoff, capped at 10 retries. do not just add a sleep(2), that degrades your throughput significantly at scale.

async function connectWithRetry(wsUrl, retries = 10) {
  for (let i = 0; i < retries; i++) {
    try {
      return await puppeteer.connect({ browserWSEndpoint: wsUrl, defaultViewport: null });
    } catch (e) {
      if (i === retries - 1) throw e;
      await new Promise(r => setTimeout(r, 150));
    }
  }
}

3. cookie sync on crash

if your Node.js process dies (SIGKILL, out of memory, etc.) while a browser is open, the vendor client does not get a chance to call the stop API. cookies that changed during that session may not be persisted. AdsPower is the worst here: it syncs cookies on stop, not continuously. Multilogin X syncs more aggressively. GoLogin falls in between. the mitigation is process signal handling: catch SIGTERM, SIGINT, and uncaughtException, then call stop on all open profiles before exiting. for truly critical sessions, export cookies via CDP’s Network.getCookies and store them externally before the session ends.

4. plan-level concurrent limits vs OS resource limits

you might have an AdsPower plan that technically allows 50 concurrent profiles but your machine runs out of memory at 12. the API will happily start profile 13 and either crash the host or make all 13 browsers unusable. monitor resident memory per profile during testing and set a hard cap in your code that is below both the plan limit and the OS limit. on a 16GB machine running nothing else, 8-10 concurrent Chromium profiles is a safe ceiling.

5. vendor API versioning and breaking changes

all three vendors have changed their local API in breaking ways at least once in the past two years. AdsPower changed the response format for /browser/start when moving to their 4.x client. Multilogin switched auth models entirely when launching Multilogin X. GoLogin has changed port numbers. pin your vendor client version and treat upgrades as deployment events that require regression testing against your automation suite. do not auto-update the desktop app on machines running production jobs.

if you are curious how airdrop farmers handle these versioning risks across large profile fleets, airdropfarming.org has some practical notes on fleet management from an operator perspective.

what we learned in production

running these three platforms simultaneously for about eight months across different use cases gave me a clearer picture of where each one is genuinely better. Multilogin X is the most consistent for long-running sessions (multi-hour workflows) because the agent is a proper service with restart capability, not a GUI app that you need to keep open. the authentication requirement is annoying to implement but makes the API harder to accidentally expose. the €99-399/month pricing is the real barrier: you are paying for fingerprint quality and support, not API features.

AdsPower wins on price-to-profile-count ratio and API ergonomics. the dual-endpoint response (puppeteer + selenium in one call) saves real code complexity. the local DNS trick is a small but genuinely useful detail. the RPA feature built into the AdsPower client is interesting for non-programmers but irrelevant once you are writing your own automation. the free tier (2 profiles) is actually usable for development, which Multilogin does not offer. for a fuller comparison of their profile features, the AdsPower review on this site covers the fingerprint config options in depth.

GoLogin’s cloud API is the unique offering here. if you are running automation on a headless server where you cannot install a desktop app, GoLogin lets you start profiles via their hosted API (with auth via API key), then connect via a remote Playwright browser that they spin up. this is more expensive per session and adds latency, but it is the only way to run these tools on a bare Linux server without a display server. the GoLogin review on this site covers the cloud vs local tradeoffs in more detail, and the Multilogin review covers the headless deployment patterns for that platform.

one thing that surprised me: error messages are a real differentiator. Multilogin’s agent returns structured JSON errors with codes that you can programmatically handle. AdsPower’s errors are human-readable strings that are inconsistent across versions. GoLogin’s errors are terse and occasionally misleading. this sounds minor until you are debugging a silent failure in a cron job at midnight and your logs contain “error: failed” with no further context.

the last thing i would flag for anyone building serious automation: the local APIs are all unauthenticated by default (except Multilogin). if your automation server is accessible from any network interface other than loopback, bind the vendor client to 127.0.0.1 explicitly in its settings. i have seen setups where the AdsPower port was reachable from the LAN because the host firewall was misconfigured, which means anyone on the same network could start and stop profiles. this is not a theoretical risk.

references and further reading

Written by Xavier Fok

disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-05-19.

need infra for this today?