Three glowing modules chained above a grid of real smartphones, captioned Playwright stealth, on a real device

Playwright Stealth on Real Devices: the Archonum JavaScript SDK

Archonum published three JavaScript packages to npm on 5 August 2026: @archonum/sdk, @archonum/engine and @archonum/cli, all at version 1.0.0, all MIT-licensed. They attach Playwright or Puppeteer to a Chrome that is already running on a real Android phone, over the Archonum CDP gateway on gateway.archonum.com:10900. Nothing runs locally. The browser is remote, and the device it runs on owns the residential IP the traffic leaves from.

If you currently maintain a Playwright stealth setup, whether that is a plugin, a pile of init scripts or a patched Chromium build, the interesting part of this release is what you get to delete.

250,000+
Real consumer smartphones the gateway hands sessions out from
archonum.com, checked 5 Aug 2026
175+
Countries available as a per-session exit
archonum.com, checked 5 Aug 2026
0
Runtime dependencies in @archonum/sdk
npm registry, @archonum/sdk 1.0.0, 5 Aug 2026

What can a Playwright stealth plugin actually patch?

Stealth plugins exist because a headless browser on a cloud host disagrees with itself. The user agent claims a phone, the GPU strings say SwiftShader, the timezone says the datacenter, and navigator.webdriver says the quiet part out loud. Each patch closes one of those gaps.

The gap that patching struggles with sits below the page. Automation libraries drive Chrome through the DevTools Protocol, and to track execution contexts they call Runtime.enable. The rebrowser-patches project documents the consequence: a few lines of JavaScript on the page fire only if Runtime.enable was used, and the technique is, in its words, “used by all major anti-bot software such as Cloudflare, DataDome, and others”. Both Puppeteer and Playwright are affected on Chrome, and DataDome’s own write-up on the CDP signal covers the same ground from the detection side.

So the detection surface is larger than what the browser reports about itself. It also includes the protocol you are driving it with, and the machine underneath it.

What the SDK does: one call, then your script is unchanged

@archonum/sdk is the credential layer and nothing else. It authenticates against the gateway, absorbs the transient 502s that a shared gateway throws during a handshake, and returns a credentialed CDP WebSocket URL. Zero runtime dependencies, ESM only.

import { ArchonumClient } from '@archonum/sdk';
import { chromium } from 'playwright';

const client = ArchonumClient.fromEnv();
const browser = await chromium.connectOverCDP(await client.getCDPUrl());

fromEnv() reads ARCHONUM_USERNAME and ARCHONUM_TOKEN, falling back to a .env in the working directory. Real environment variables always win over the file. If credentials are missing, the client fails immediately with a one-line error instead of hanging on a connect.

Everything after that second line is your existing script. The browser on the far end is Chrome for Android on a phone that owns its residential IP, so the browser and the exit IP are the same device, and there is no second machine whose fingerprint has to be reconciled with the story the browser tells.

Two more calls round the client out. getHealth() is an unauthenticated liveness probe, and getCredits() returns the remaining balance in MB for the account. Pass country as an ISO 3166-1 alpha-2 code to pin the exit; leave it empty and the gateway chooses.

One handling note the type definitions are explicit about: the gateway authenticates via query-string parameters, so the token ends up inside the URL that getCDPUrl() returns. Keep credentialed URLs out of logs and out of anything you persist.

What does the engine add for agents that drive pages?

@archonum/engine sits on the SDK and owns everything above the connection: the retrying handshake, sessions, and the helpers an agent needs when it is interacting rather than fetching. It ships one dependency, a pinned puppeteer-core 25.4.0.

The one-shot read is where most agent code starts. It is a fetch that runs client-side JavaScript and can read a page as seen from a given country:

import { ArchonumEngine } from '@archonum/engine';

const engine = ArchonumEngine.fromEnv();
const { text, settled, blocked } = await engine.read('https://example.com/', {
  country: 'de',
  waitForText: 'Add to cart',
});

For interaction, connect() returns a session, and the session speaks in accessibility refs rather than selectors. snapshot(page) returns a compact outline, lines like [e42] textbox "Search", and click, type, select, press and scroll act on those refs. It costs text tokens instead of vision tokens, and it pierces open shadow roots and same-origin iframes. Cross-origin frames cannot be read, so the snapshot prints a marker line for each one rather than quietly leaving it out.

Three behaviours matter more than the method list, because they decide whether an agent gets stuck in a loop.

  • Actions report what they did. click() returns navigation, dom-change or none. A none usually means the widget ignores synthetic mouse events and wants keyboard activation, and fallback: true retries via focus and Enter.
  • Waiting has a stall detector. waitForContent() polls for rendered text, but returns stalled: true as soon as a page shows no DOM mutation, no network request and no text change for the stall window. That is the signature of a block that will never clear, and it comes back at once instead of burning the full timeout.
  • Blocked pages get classified. pageInfo() returns a challenge kind (cloudflare-turnstile, akamai, perimeterx, datadome, generic-interstitial, unknown-empty) alongside a clearable rating of likely, maybe or unlikely. A real device waiting out a Cloudflare JS challenge usually gets through it. An unknown-empty page never will, and the agent should stop waiting.

The blocked heuristic is deliberately conservative. Short page text on its own does not count as evidence: https://example.com/ renders 129 characters and is a perfectly healthy page, and reporting it as blocked would send an agent off to rotate identity for no reason.

compareFlow() is the call with no single-device equivalent. It replays the same declarative flow across several exit countries in parallel, each in its own session, and returns one result per country, including a value pulled from a CSS selector if you pass one.

const results = await engine.compareFlow('https://shop.example/', {
  countries: ['ch', 'de', 'us'],
  extract: '.price',
  screenshot: true,
});

Because it replays a real interactive flow instead of fetching a homepage three times, it survives multi-step targets: search, filter, then read the price a customer in that country actually sees.

What we deliberately did not spoof

One patch does survive, and it is ours to maintain rather than yours. The engine ships a locally patched Puppeteer that avoids the Runtime.enable leak described above, and that single patch is the entire stealth layer. What disappears is the stack on your side: the evasion plugin, the init scripts, and the job of re-testing all of it every time Chrome ships a release.

We do not spoof navigator.webdriver, the plugin list, WebGL or GPU strings, or the user agent. Our own measurement was that tampering made a legitimate real-device fingerprint look worse rather than better, which is roughly what you would expect. Every patch is another chance for two signals to disagree, and disagreement is what the detectors are scoring.

We do not spoof navigator.webdriver, the plugin list, WebGL or GPU strings, or the user agent.

The same reasoning governs desktop mode. newPage({ desktop: true }) widens the viewport and drops the Mobile token, but it keeps the device’s real user agent, because inventing a Windows identity on an Android handset is exactly the contradiction a wall is looking for. Sites that gate their mobile layout behind an app, with taobao.com as the standing example, are the reason the flag exists at all.

Scope note

The Archonum fleet is Android. Every session is Chrome for Android on a real handset, so sites render their mobile layout by default, and desktop mode is a wider viewport on that same phone rather than a desktop machine. Billing does not change with the SDK: your plan buys a concurrency tier, and bandwidth and browser runtime meter on top of it.

Which of the three packages should you install?

@archonum/sdk
You already have a working Playwright or Puppeteer script

Take the credentialed CDP URL and hand it to connectOverCDP. Two added lines, zero dependencies, and your automation code is untouched. Nothing else in this release is required.

@archonum/engine
You are building an agent that drives pages it has not seen

Snapshots, effect-reporting actions, stall detection and challenge classification are the parts you would otherwise write yourself, badly, after the third production incident.

@archonum/cli
You want to check one page from a terminal

npx @archonum/cli https://example.com screenshots through a real device and writes example.com.png. Useful when the question is what a page looks like from somewhere else.

If your agent speaks MCP rather than JavaScript, the hosted Archonum MCP server at app.archonum.com/mcp exposes the same engine with nothing to install.

The Archonum JavaScript SDK is on GitHub at Archonum/archonum-sdk-js under the MIT license, and the gateway reference lives in the Archonum docs. An account and an API token from app.archonum.com/register are the only prerequisites. Plans start at $24.99 per month for 25 concurrent sessions, with bandwidth and browser runtime metered separately.