Browser automation for agents is a fingerprint problem. Puppeteer and Playwright abstract the Chrome DevTools Protocol (CDP) into high-level APIs, but that abstraction leaks detection signals. Mochi.js takes the opposite approach: Bun-native, raw CDP access, and a relational fingerprint engine that treats every browser surface as a derived artifact of a single seed.
This matters for web scraping agents, autonomous browsing systems, and any RPA workflow that needs to survive bot detection. The Show HN post drew 47 points and active discussion because it exposes a gap in the tooling stack: most automation libraries optimize for developer ergonomics, not for passing getParameter(0x9245) probes.
What Mochi.js Actually Does
Mochi.js is a Bun runtime library that speaks CDP directly. No Node.js, no Python sidecars, no FFI to install. You pass a profile (e.g., linux-chrome-stable) and a seed (e.g., user-12345), and it generates a coherent fingerprint across canvas, WebGL, audio, fonts, MediaDevices, and WebGPU.
Key architectural choices:
- Relational consistency engine: A 48-rule directed acyclic graph (DAG) ensures that a macOS user agent never appears alongside Linux WebGL parameters. Every fingerprint surface derives from the same (profile, seed) pair.
- Chromium-native fetch:
session.fetch()routes through CDP’sNetwork.loadNetworkResourcefor GETs andpage.evaluate('fetch')for non-GET requests. JA4, JA3, and HTTP/2 fingerprints are real Chrome by definition. - Behavioral synthesis:
humanClick,humanType, andhumanScrolluse Bezier curves with overshoot correction, Fitts-law movement times, and lognormal digraph delays. Profile parameters include hand dominance, tremor, words per minute, and scroll style. - Probe-Manifest harness: Captured baselines from real devices live in the repo. CI diffs the live session’s probe manifest against the baseline. Zero-diff is a gate.
CDP Access vs. Abstraction Layers
Puppeteer and Playwright wrap CDP in a stable API. That stability comes at a cost: you cannot control low-level protocol timing, header ordering, or resource load sequencing without monkey-patching internals.
Mochi.js exposes raw CDP commands. This gives you:
- Direct control over
Network.setUserAgentOverride,Emulation.setDeviceMetricsOverride, andPage.addScriptToEvaluateOnNewDocument. - The ability to inject payloads before any page script runs, avoiding race conditions that leak detection signals.
- Full visibility into CDP event streams, so you can log or replay every protocol message for debugging.
The trade-off: you write more boilerplate. Mochi.js provides helpers for common patterns (launch, newPage, goto), but you are closer to the metal.
Fingerprint Coherence as a First-Class Concern
Most fingerprint spoofing libraries patch individual surfaces. You set a canvas fingerprint here, a WebGL vendor string there, and hope the combination looks plausible. Mochi.js treats coherence as a constraint satisfaction problem.
The relational consistency engine enforces rules like:
- If
navigator.platformisMacIntel, thenWebGLRenderingContext.getParameter(UNMASKED_VENDOR_WEBGL)must return an Apple GPU. - If
navigator.hardwareConcurrencyis 8, thenperformance.memory.jsHeapSizeLimitmust fall within a plausible range for that core count. - If
AudioContext.sampleRateis 48000, thennavigator.mediaDevices.enumerateDevices()must return audio input devices that support that rate.
This is implemented as a DAG where each node is a fingerprint surface and edges represent derivation rules. The seed deterministically walks the graph, so the same (profile, seed) pair always produces the same fingerprint.
Bun Runtime Performance Characteristics
Bun’s native runtime affects two dimensions: startup time and memory footprint.
Startup time: Bun’s JavaScriptCore engine starts faster than Node.js’s V8 for short-lived scripts. For long-running browser sessions, this advantage disappears after the first few seconds.
Memory footprint: Bun’s FFI layer is lighter than Node.js’s native addon system. Mochi.js avoids FFI entirely by using CDP for all browser communication, so the memory difference is marginal. The real win is eliminating the Python or Node.js sidecar that many fingerprint libraries require.
Concurrency: Bun’s event loop handles concurrent CDP connections efficiently. If you are running 50 browser sessions in parallel, Bun’s lower per-connection overhead matters. For single-session automation, the difference is negligible.
Architecture: Fetch Routing and Injection Timing
Mochi.js routes HTTP requests through Chromium itself to preserve JA4 and HTTP/2 fingerprints. Here is the flow:
- Simple GETs:
session.fetch(url)callsNetwork.loadNetworkResourcevia CDP. Chromium’s network stack handles TLS negotiation, header ordering, and HTTP/2 framing. - Non-GET requests:
session.fetch(url, { method: 'POST', body })injects afetch()call into the page context viaRuntime.evaluate. The request goes through Chromium’s fetch implementation, not a parallel HTTP client. - Payload injection:
Page.addScriptToEvaluateOnNewDocumentruns before any page script. This is where Mochi.js injects fingerprint overrides. The timing guarantee prevents detection via script execution order.
| Approach | JA4 Fidelity | Timing Control | Maintenance Burden |
|---|---|---|---|
| Raw CDP (Mochi.js) | Native Chrome | Full | High (protocol changes) |
| Puppeteer/Playwright | Native Chrome | Limited | Low (stable API) |
| curl-impersonate | Emulated | Full | Medium (TLS updates) |
| Python requests + patches | Broken | None | High (constant drift) |
Behavioral Synthesis: Bezier Paths and Fitts Law
humanClick(x, y) does not teleport the cursor. It generates a Bezier curve from the current position to the target, with overshoot and correction. Movement time follows Fitts’s law:
T = a + b * log2(D / W + 1)
Where D is distance, W is target width, and a, b are profile-specific constants. The profile also encodes hand dominance (affects curve bias), tremor (adds noise), and click duration (time between mousedown and mouseup).
humanType(text) uses lognormal distributions for digraph delays. Common bigrams like “th” or “er” have shorter delays than rare pairs. The profile’s wpm parameter scales the entire distribution.
humanScroll(distance) synthesizes scroll events with easing curves. The profile’s scrollStyle parameter chooses between smooth (trackpad-like) and stepped (mouse wheel-like) behavior.
This is overkill for most automation tasks. It matters when you are bypassing behavioral analysis systems that flag robotic timing patterns.
Probe-Manifest Harness and CI Gates
The probe-manifest harness captures a baseline fingerprint from a real device. The baseline includes:
- Canvas
toDataURL()output for a test pattern. - WebGL
getParameter()results for all enumerated constants. - Audio context fingerprint (oscillator output hash).
- Font enumeration results.
- MediaDevices list.
Every CI run launches a Mochi.js session, runs the same probes, and diffs the output against the baseline. If the diff is non-zero, the build fails unless the divergence is documented in a rationale file.
This catches regressions where a CDP protocol change or a Chromium update breaks fingerprint coherence. It also prevents accidental leaks where a new feature (e.g., WebGPU support) is added without updating the relational consistency rules.
Failure Modes and Observability Gaps
CDP protocol drift: Chrome updates the CDP schema every six weeks. Mochi.js tracks the stable subset, but new detection vectors (e.g., WebGPU fingerprinting) require manual rule additions.
Timing side channels: Even with behavioral synthesis, network timing can leak bot signals. If your agent makes 50 requests in 100ms, no amount of cursor wiggling will help. Mochi.js does not throttle requests automatically.
Headless detection: Chromium’s headless mode sets navigator.webdriver to true and exposes other signals. Mochi.js launches headed Chrome by default, but you can override this. Headed mode requires a display server (Xvfb on Linux).
Observability: Mochi.js logs CDP events to stdout, but there is no built-in tracing or metrics export. For production agent fleets, you need to wrap it in your own telemetry layer.
Code Example: Stealth Session with Fingerprint Seed
import { mochi } from "@mochi.js/core";
const session = await mochi.launch({
profile: "linux-chrome-stable",
seed: "user-12345", // Deterministic fingerprint
headless: false, // Avoid headless detection
});
const page = await session.newPage();
// Inject payload before page scripts run
await page.addInitScript(() => {
// Override navigator.webdriver
Object.defineProperty(navigator, "webdriver", {
get: () => false,
});
});
await page.goto("https://example.com");
// Behavioral synthesis
await page.humanClick(100, 200);
await page.humanType("search query");
await page.humanScroll(500);
// Chromium-native fetch
const response = await session.fetch("https://api.example.com/data");
console.log(await response.text());
await session.close();
When to Use Mochi.js vs. Puppeteer
Use Mochi.js when:
- You need to bypass bot detection systems that fingerprint WebGL, canvas, or audio.
- You are building autonomous browsing agents that must survive behavioral analysis.
- You want deterministic fingerprints for testing or replay.
- You are already on Bun and want to avoid Node.js dependencies.
Use Puppeteer/Playwright when:
- You are scraping sites with minimal bot detection.
- You need a stable API that does not break with Chrome updates.
- You want a large ecosystem of plugins and integrations.
- You do not care about JA4 or HTTP/2 fingerprints.
Avoid Mochi.js when:
- You need cross-browser support (Firefox, Safari). Mochi.js is Chromium-only.
- You are running on Node.js and cannot switch to Bun.
- You need a mature, battle-tested library with enterprise support.
Technical Verdict
Mochi.js is infrastructure for high-fidelity browser automation. It trades developer ergonomics for detection evasion. If your agent needs to pass canvas fingerprinting, WebGL probes, or behavioral analysis, the raw CDP access and relational fingerprint engine are worth the boilerplate. If you are scraping public data from sites with minimal defenses, Puppeteer is faster to ship.
The Bun-native design is a forcing function: it eliminates the Node.js/Python sidecar pattern and pushes all logic into a single runtime. This simplifies deployment but locks you into Bun’s ecosystem.
The probe-manifest harness is the most interesting piece. It turns fingerprint coherence into a testable property. Most automation libraries treat detection evasion as a best-effort hack. Mochi.js makes it a CI gate.
Use it when you need to survive strict bot detection. Skip it when you need to ship fast and the target does not care.