04 · REST API

Humanized Playwright with fingerprint control.

View as Markdown

Two things share the Wayfern name, and it helps to keep them straight:

  • Wayfern — the anti-detect Chromium engine that powers Donut Browser. Fingerprint spoofing (UA, platform, timezone, language, WebGL, canvas, audio, WebRTC, …) is enforced at the C++ layer, exposed via custom CDP commands. Headless from the JS side, you can’t tell it apart from Chrome.
  • wayfern (npm) — the open-source helper library on top of Playwright. It transparently humanizes mouse, typing, and scroll, and exposes thin wrappers around the engine’s fingerprint CDP commands. Used in production by Donut Browser itself and MIT-licensed.

The two are independent: you can use the npm package against any Playwright-driven Chromium, and you can drive Wayfern’s engine over plain CDP without the helper. They are designed to work together though — pair them and you get humanized actions against an engine whose fingerprint actually matches what it claims to be.

Install

npm install wayfern playwright

Drop-in mode

Replace your Playwright import and every page is auto-humanized. Existing Playwright code keeps working unchanged.

import { chromium } from "wayfern";

const browser = await chromium.launch();
const page = await browser.newPage();

await page.click("button");
await page.locator("input").hover();
await page.keyboard.type("hello world");
await page.mouse.wheel(0, 800);

Explicit mode

For granular control — typing speed, scrolling targets, fingerprint commands — attach explicitly:

import { chromium } from "playwright";
import { wayfern } from "wayfern";

const page = await (await chromium.launch()).newPage();
const wfp = wayfern(page);

await page.click("button");                   // still humanized via hooks
await wfp.typeText("hello", { wpm: 90 });
await wfp.scrollTo("footer");
await wfp.refreshFingerprint({ operatingSystem: "macos" });
const fp = await wfp.getFingerprint();

Attach to a running Donut profile

The most common pattern: launch a profile via the REST API (which returns the remote_debugging_port), then connect with a plain Playwright call. No raw CDP wiring needed — wayfern re-exports Playwright so the snippet stays idiomatic.

// 'wayfern' re-exports playwright with auto-humanized hooks,
// so this is just a normal Playwright connection.
import { chromium } from "wayfern";

// remote_debugging_port comes from POST /v1/profiles/{id}/run.
// See https://donutbrowser.com/docs/rest-api#run for the launch call.
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
const page = browser.contexts()[0].pages()[0];

// Every page is already humanized. Drive it like vanilla Playwright:
await page.goto("https://example.com");
await page.click("button#login");
await page.fill("input[name=email]", "[email protected]");

API surface

wfp.move(x, y, opts)              // duration, overshoot (0–1)
wfp.hover(target, opts)
wfp.click(target, opts)           // button, clickCount, delay, position, hoverPause
wfp.dblclick(target, opts)
wfp.clickAt(x, y, opts)
wfp.type(target, text, opts)      // wpm, layout ('qwerty'|'azerty'), errors, clear, focus
wfp.typeText(text, opts)
wfp.fillForm(values, opts)        // {selector: value} or [[sel, val], …] — tabs between fields
wfp.scroll(dx, dy, opts)
wfp.scrollBy(dy, opts)
wfp.scrollTo(target, opts)
wfp.idle(ms, opts)                // micro-jitter the cursor for ms
wfp.pause(min, max)
wfp.refreshFingerprint(params)    // operatingSystem, timezone, language, latitude, longitude
wfp.getFingerprint()              // full fingerprint payload from the engine
wfp.cdp.send(method, params)      // generic CDP escape hatch

Clicks insert a small “hover-before-click” pause when the cursor traveled meaningful distance to the target, mimicking visual confirmation before pressing. Disable per-call with { hoverPause: false }.

Opting out

Per call, per page, or for individual operations:

// Bypass humanization for one call
await page.click("button", { force: true });
await page.locator("input").hover({ trial: true });

// Disable hooks entirely on this page
const wfp = wayfern(page, { hooks: false });

// Disable hover-before-click on one call
await wfp.click("button", { hoverPause: false });