A page can tell a script's scroll from a person's in two lines of JavaScript. A scroll made with window.scrollBy fires scroll events with no wheel event in front of them, and a click made with element.click() arrives with isTrusted set to false and no pointer events around it. Our warm-up robot did both, on every site it visited, for as long as it existed in that form.

What the robot was for

Warm-up browses a profile before it meets the site that matters. A profile with an empty cookie jar and no history reads as brand new, and "no prior relationship with the web" is a risk signal on its own, however good the fingerprint is. So the robot opens the profile's own browser, with its own proxy, visits a list of ordinary sites, scrolls, follows a few links and lets each site store what it normally stores.

The first version did that with JavaScript evaluated in the page. Scrolling was window.scrollBy in a loop. Clicking a link or a consent button was node.click(). Both did exactly what they said. Cookies arrived, history filled up, the run reported success.

That is why it survived. It worked.

isTrusted false and scroll detection: how a page sees a script

Every input a person makes reaches the page as a sequence. A wheel notch produces a wheel event, then the page scrolls, then scroll fires. A click is a pointermove trail, pointerdown, mousedown, pointerup, mouseup, then click, with focus moving on the way. Each of those events carries isTrusted: true, because the browser made it.

A script skips most of the sequence. Here is the whole test a page needs:

let lastWheel = 0
addEventListener('wheel', () => { lastWheel = performance.now() }, { passive: true })
addEventListener('scroll', () => {
  if (performance.now() - lastWheel > 200) report('scroll without a wheel')
})
addEventListener('click', (e) => { if (!e.isTrusted) report('untrusted click') }, true)

A real page would also allow for keyboard scrolling, a dragged scrollbar and touch, so a careful one looks at several signals together rather than one. Our robot failed all of them at once: no wheel, no key, no touch, and the clicks untrusted.

Give a new profile a history before it meets the site that mattersIn Mosaic: Warm-up robot. A browsing history before the site that matters. Free holds 3 profiles. Pro is $49 a month for any number.

How it works: Warm-up robot

Why that was worse than no warm-up

The robot exists to make a profile look used. Instead it wrote a behavioural flag into the history of every profile it touched, on the same sites whose cookies it was collecting. Those sites now held a record of a visitor who scrolled like a script, linked by cookie to the profile that would come back later.

A fingerprint is a set of values. Behaviour is the half that no value hides, and the robot was writing the wrong behaviour into the place the profile's reputation lives. We wrote it down at the time as "a behavioural flag in the jar, called a reputation".

What replaced it

Every gesture now goes through human-input.ts, which sends input at the browser level over the DevTools protocol (Input.dispatchMouseEvent and Input.dispatchKeyEvent). The page receives the whole sequence with isTrusted: true, the same path a physical mouse takes.

Trusted is necessary, not sufficient. A trusted cursor that moves in a straight line at constant speed and lands dead centre every time is still not a hand. So the motion follows the models of real pointing: Fitts' law for how long a move takes, a minimum-jerk curve for its shape, an occasional overshoot and correction on long moves, and sub-pixel tremor on every sample. The cursor also keeps its position between actions, so a click never arrives from somewhere the pointer has not been.

Scrolling is a real wheel. Each profile is a trackpad or a notched wheel, chosen once from its seed, because a session that scrolls like a trackpad on one page and a mouse on the next is describing two computers. A mobile profile gets a finger instead: touch events and a fling, no hover and no wheel at all.

The Mosaic warm-up dialog for one profile: a preset, a site count and seconds on each site
Warm-up: same browser, same proxy, input through the pipeline

We nearly made the same mistake twice

Page control, the part of Mosaic that lets a script or an AI assistant click on a page, needs to know where an element is before it clicks. Its first version called scrollIntoView to bring the element on screen and then clicked it with a trusted event. That would have put an untrusted scroll with no wheel immediately before every trusted click: a worse signature than either on its own, because the pair says a script aimed and then pretended.

It now reports whether the element is in view and how far away it is, and moves nothing. The caller turns a real wheel and measures again. For the one case a document scroll genuinely cannot reach, it falls back to a programmatic scroll and says so in the result, as forced: true, so nobody mistakes it for the clean path.

Where the rest of the warm-up stands

The sending path was measured as well as reasoned about. A CDP detector that flips to "bot" when the protocol's Runtime domain is enabled stayed clean when input went through the Input domain, so the trusted events cost nothing on that side.

Warm-up runs in the profile's own browser, with the fingerprint described in real fingerprints, so the cookies it earns belong to the machine that will spend them. Why separate profiles need separate histories at all, alongside cookies and IPs, is in how accounts get linked. And when an assistant drives the same robot over MCP, it goes through the same pipeline: letting an AI assistant run browser profiles has how that door is held to the window's rules.