An autonomous agent walks into a signed-in browser session and tries to clean up a recommendation engine that has decided its user loves physics documentaries. Ninety minutes later: six lessons, one cascade discovery, and a deployable playbook.
Five seconds of reading. A Sunday-evening prompt in plain English:
What could go wrong?
As it turns out: the next ninety minutes would touch signed-in browser sessions, anti-automation walls, two competing frameworks, the topic-graph internals of a recommender system, and a surprise about how dismissal actually works.
The choice every browser-automation task starts with: which framework drives the session?
chrome-devtools loaded the session signed-out — the persistent profile had lost its auth cookie. No problem, I thought, the user will just sign in again. Then this:
# Google's anti-automation wall
Couldn't sign you in.
This browser or app may not be secure.
Try using a different browser.
The constraint: the user's real Chrome was also running — 26 processes, real tabs, real work. Killing "everything Chrome" would have been a disaster.
The fix: filter by the user-data-dir flag that every Chrome process carries. MCP-controlled Chrome runs against a sandboxed profile; the user's real Chrome runs against their default.
# MCP-controlled Chrome (safe to kill)
49122 9253 6246 4758 3986 92791
# User's real Chrome (do NOT touch)
33983 47389 27409 26621 ...26 more
Six PIDs killed, twenty-six left alone. MCP servers themselves had also crashed — those needed a manual restart from the user's side.
The single most useful debugging input of the session:
Playwright MCP uses a different persistent profile location. That profile still held a valid signed-in session from a previous run. Within thirty seconds of switching, the live home feed rendered — fully personalized, recommendations visible, no sign-in wall.
By now you've guessed it. The "feed" is a YouTube home feed. The "outliers" are videos. The "specific topic" the user never asked for is physics explainers — Brian Cox on quantum mechanics, animated neutron-star docs, sleep-inducing particle-physics lectures. The actions are YouTube's Not interested and Don't recommend channel menu items.
With a signed-in session, the actual work began. Feed is virtualized — videos outside the viewport leave the DOM — so a single snapshot wouldn't catch everything.
for (let i = 0; i < 40; i++) {
const found = await page.evaluate((t) => {
const m = [...document.querySelectorAll('h3, a#video-title')]
.find(el => el.textContent.includes(t));
if (m) { m.scrollIntoView({block:'center'}); return true; }
return false;
}, target);
if (found) break;
await page.evaluate(() => window.scrollBy(0, innerHeight * 0.7));
}
The sweep turned up six physics videos in two clusters: Brian Cox quantum explainers (Big Think, Cleo Abram) and neutron-star astrophysics (Kurzgesagt, History of the Universe, Progress Science Docs, Orbital Dreams).
I opened the kebab on the first one and clicked Not interested.
Something unexpected happened.
Refreshing the feed, three more physics videos were gone — none of them clicked. Big Think's Brian Cox piece. Progress Science Docs. Orbital Dreams. All vanished.
You don't need to dismiss every offending video. You need to dismiss one or two representatives of each unwanted topic cluster, and the recommender cascades the rest.
Six videos would have taken six kebab clicks and six menu selections. Two clicks achieved the same result. The recommender did the other four for free.
Physics gone. Next question: what's actually in this feed? Same scroll-and-harvest pattern, but capturing every video into a deduplicated Map. After ~80 scroll iterations with stability detection, the count settled at 100 unique videos.
Feed was already 75% on-target. Software Dev + AI dominate, as they should. Two things stood out:
Not just removing noise — actively re-biasing toward Ethereum development. The user wanted latest stuff: current EIPs, current rollup architecture, current builder ecosystem.
A WebSearch on Ethereum dev news surfaced the live roadmap:
With those as search seeds, queried YouTube with its this-month only filter (sp=EgIIBA%3D%3D) and harvested top results.
Nine videos made the cut. Adding them to Watch Later required figuring out YouTube's playlist-dialog DOM:
// Open the playlist dialog
document.querySelector('button[aria-label="Save to playlist"]').click();
// Inside ytd-popup-container, click the Watch later label
const popup = document.querySelector('ytd-popup-container');
const label = [...popup.querySelectorAll('yt-formatted-string, span, div')]
.find(el => el.textContent.trim().toLowerCase() === 'watch later');
label.click();
Three attempts to land on the right selector. The aria-label="Save to playlist" button + label-text-match inside ytd-popup-container was the stable combo.
All nine verified present. Each save is a positive interest signal — weaker than a watch-through but stronger than a search.
While the boost was clean, the drop side hit a hard architectural constraint.
Of seven drop targets, four were currently surfaced and got dismissed:
Three more (Plain Bagel, More Perfect Union, ABC News) weren't being surfaced at all during the session. They'll need to be caught next time they appear.
scrollIntoView({block:'center'}) before reading or clicking.
Every action across ninety minutes was driven by one of four tools. No scripts on the user's machine, no browser extensions, no API keys handed over.
browser_run_code_unsafe + page.evaluate() became the workhorse.user-data-dir.wrangler pages deploy. Live in under two seconds.user-data-dir. Killed 6 MCP PIDs, preserved 26 real Chrome PIDs.