Optimizing the browser agent's tools, from its own JavaScript history

Context

Agents often use execute_javascript as an escape hatch in browser-use. The calls range from text dumps to full-on scroll-harvest jobs.

North star

I want to optimize the tools available to the agents, site-specific or not. Better latency, fewer output tokens, fewer turns. That improves the user experience and saves significant cost. I concentrate on the head-of-distribution sites to maximize impact. I use execute_javascript as the signal to work from.


SECTION 1

Investigation

1.1  Scraping the history

The entire history of Polar's JS calls, split simply by domain.

SiteJS callsRuns using JS
on this site
Users ever using JS
on this site
www.linkedin.com270,2589,9811,309
www.instagram.com90,1821,896435
www.google.com80,1471,454624
x.com57,9002,337349
internal.phia.com47,8492,02017
www.facebook.com42,7501,432366
mail.google.com39,2554,765900
app.slack.com20,7521,946129
www.tiktok.com17,914725228
www.pinterest.com17,87846026
www.youtube.com15,898660316
papers.ssrn.com12,591379
www.thedailyvirals.com11,3401111
www.kalodata.com10,0891685
drive.google.com9,9711,525314
outlook.cloud.microsoft9,754894140
www.indeed.com9,240542152
providers.joinmochi.com8,7075431
www.revolve.com7,44328213
www.zillow.com7,41433488

1,657,377 calls. 82,196 runs. 5,456 users. 44,636 sites. This is all of it: the first JavaScript call is dated 8 July 2026. These 20 sites hold 47.5% of every call. The grey rows are a handful of people's bespoke automation. Section 1.2 deals with them.

Highlights of the work


1.2  What is the actual head of distribution?

The GitHub gist I was given named a few head-of-distribution sites. But it called its own method a weak proxy. It only counted how often an https string appeared in traces. So I could not use it to find the real head of distribution.

Many other problems made this a data engineering task as much as an ML tooling task.

What failed

What worked

The true head of distribution. Top 25 sites.

#SiteJS callsRunsUsers Effective
users
Share of
top user
HHI rank
+ call rank
1www.linkedin.com270,2589,9811,309109.94.6%2
2mail.google.com39,2554,76590068.14.6%9
3x.com57,9002,33734921.015.0%16
4www.facebook.com42,7501,43236620.312.7%18
5www.tiktok.com17,91472522824.113.3%18
6www.indeed.com9,24054215231.09.0%19
7www.amazon.com7,18867124338.99.9%20
8www.reddit.com5,92158717441.27.6%20
9www.youtube.com15,89866031616.817.4%24
10drive.google.com9,9711,52531419.413.6%24
11docs.google.com3,8821,67844969.54.9%25
12chatgpt.com5,22582316223.013.8%31
13www.zillow.com7,4143348814.320.0%32
14github.com2,80371620841.27.0%32
15app.notion.com3,37350011126.28.3%33
16www.instagram.com90,1821,8964357.733.7%36
17*.instructure.com3,06874114125.913.3%36
18www.google.com80,1471,4546246.735.8%38
19app.slack.com20,7521,9461297.830.6%39
20outlook.cloud.microsoft9,7548941408.430.5%42
21jobs.ashbyhq.com3,20254515814.721.9%44
22www.ebay.com2,78431016211.326.0%51
23www.booking.com1,3331506515.814.0%51
24job-boards.greenhouse.io2,61348714313.024.2%52
25app.apollo.io5,531312575.836.6%54

Every single-user host from the first table is gone. Instagram falls from 2nd to 16th. Google falls from 3rd to 18th. One user drives about a third of each. docs.google.com is 23rd by call count alone, and 11th here. Instructure's Canvas reaches 17th once its schools merge.


1.3  Extracting the signal to automate: the crux

Now I know which sites to focus on. Next I must find what actions get expensively rewritten in JavaScript, over and over.

This is hard. The same macro-action can be written a hundred different ways. So a capability is not a byte-identical script. To find what is worth compressing into a tool, I have to group by capability. Byte-identical groups are a healthy fraction, but a very long tail sits behind them.

What I tried

None of them worked. The best covers only 34% of LinkedIn's traffic with its top 25 groups. The table scores them against two baselines: the raw script, and the script with its literals normalized. The number is that share, measured on LinkedIn.

Candidate unit for a capabilityDistinct unitsCoverage by top 25
Byte-identical script78,74221.6%
Script with literals normalized59,22825.4%
AST shape—34.0%
MinHash leader clustering—33.9%
The agent's own code description139,0906.2%

So I used a special workflow

  1. Clean. Remove the comments. Lint every script into the same form. Drop the scalar literals. That string is the crux.
  2. Drop the trivia. A crux over 200 characters always counts. Below that I drop bare property reads, bare calls, and anything under 60 characters that projects nothing. That removes 102,555 calls.
  3. Group on the crux. Identical, or 95% similar.
  4. Rank on how many times that same script was called across runs.
  5. Extract, 5× per site. Sample 150 scripts at random, weighted toward higher rank and popularity. Give them to a Haiku 5.5 at default effort. Ask it for every capability, as a list of parametrized JS scripts. Tell it to favor low latency and token-efficient returns. Do this for all 25 sites.
  6. Merge. Opus agents and fresh Haikus take the 125 reports. They extract the JS functions common within one site, then across sites.

That is 125 claude -p runs. They read 18,750 scripts and proposed 1,434 capabilities with code. Every run went in parallel. It took 14 minutes, and only 10% of a subscription's five-hour window. In series it would have taken 27 hours.


SECTION 2

Delivered to the harness

2.1  High-signal new general tools, and improvements to existing ones

Six changes. Three new tools, and three improvements to tools that already exist. All six are built, registered and verified in Polar's own Chromium.

What pain does this help with? Every frequency below covers all runs, not just the head of distribution.

FixSignal it replacesCalls% of
calls
Distinct
scripts
Users% of
users
Fails
today
What it removes
harvest_listnew tool Scroll, read, dedupe loops. One model turn per step. 45,1202.7%21,9911,45227%8.8% 20,252 model turns. That is 1.3% of the turns that ran JavaScript, and 33.5 hours of waiting.
get_linksnew tool a[href], then .map, then .filter. Written again each time. 199,33012.1%107,6183,56266%7.5% 24.3 M characters of authoring.
read_formnew tool Sweeps over input, select and textarea for values, labels and errors. 43,5082.6%32,8031,80133%5.2% Repeated reads of a form that does not fit the viewport.
wait_foron execute_javascript setTimeout(…, 1500), then read. A blind guess at how long the page needs. 21,0361.3%11,6241,03919%11.6% Part of 33.7 hours of fixed sleep. It returns when the selector appears.
empty-result markingon execute_javascript [] and no reason. The page was empty, or the selector was wrong. 59,0393.6%34,2373,31461%all of them 12,387 re-runs that come back empty again.
patternon get_page_text document.body.innerText and a regex. Grep, hand-rolled in JS. 57,1403.5%32,5372,35444%5.2% A 300-character regex per call, and the line numbers to read around a hit.

Measured over 1,648,945 calls, 80,916 runs, 5,409 users and 44,635 sites. These are the calls made on a site; the other 8,432 ran on no page. A match needs the recovered script text. That exists for 81% of calls. So every count here is a lower bound.

Fails today is the share of those calls that do not return ok. The corpus baseline is 6.2%. The empty-result row is the exception. Those all fail by definition, so the rate says nothing.

Measuring the actual pain point

  • 3,695 hof user waiting per quarter, and $12,697 billed, on six jobs the agent writes JavaScript for because the browser gives it no command to call. 447,060 calls, 4,635 users, 31,398 sites.↳ addressed by harvest_list, get_links, read_form, and wait_for / empty-result marking / pattern on the existing tools — section 2.1
  • 1,548 hIt wants the links on the page. There is no command for that, so it types out [...document.querySelectorAll('a')].map(a => a.href) and a filter, and ships it to the browser. It wrote that program 170,440 times.↳ addressed by get_links(href_pattern=) — one call, no script
  • 615 hThe element it needs is not in the DOM yet, and nothing tells it when it will be. So it guesses: await new Promise(r => setTimeout(r, 3000)). 96 of those hours were spent inside setTimeout on pages that were already ready.↳ addressed by execute_javascript(wait_for=) — returns the moment the node appears
  • 316 hMarketplace only loads more listings as you scroll. So the agent runs window.scrollTo(0, document.body.scrollHeight), reads what appeared, scrolls again — and every single scroll is a separate round trip to the model. One page took 422 of them.↳ addressed by harvest_list(item_selector=, key=) — the whole loop runs in-page and dedupes on key
  • 313 hdocument.querySelectorAll('.listing') matched nothing, so the call returned []. The agent cannot tell whether the page really had no listings or whether .listing is just the wrong class name. 59,039 calls came back like this.↳ addressed by empty-result marking on execute_javascript

Jul 8 – Oct 8, 2026. Hours are measured elapsed time from each call to the next, capped at 120s so an idle user is not counted. Dollars at the billed $0.0284 per tool call.


2.2  High-signal new site-specific toolkits

The six changes above supply the scrolling, the link scan and the form read. They do not supply the meaning. The code that turns one card or one row into named fields shares no line between sites. A site toolkit supplies that.

Built from the 25 site reports in section 1.3: 26 toolkits, 100 verified readers, each one derived from the production scripts for that page. The agent calls them with execute_javascript(toolkit="<site>") instead of writing DOM code.

amazon.com — pxAmazon
  • searchResults: one row per product card on a search page
  • product: the product page as typed fields
  • productDetails: the Product details block as label to value
  • reviews: the customer reviews rendered on screen
  • reviewSummary: star histogram, generated summary, topic mention counts
  • bestSellers: a Best Sellers category page in rank order
  • orders: the orders shown on your-orders
  • cart: active cart items
  • variants: size and color options
  • images: gallery images with hi-res URLs
facebook.com — pxFacebook
  • eventFacts: an event page's facts
  • eventDescription: the event description text
  • listingDetail: an open Marketplace listing
  • openPost: a post open in a dialog
  • postPhotos: full-size photo URLs for a post
google.com — pxGoogle
  • place: the open Maps place panel: name, address, phones in E.164, website, rating, reviews
  • serp: organic web results with rank, title, URL, host, plus People Also Ask
  • aiOverview: the AI Overview block and its cited sources
youtube.com — pxYoutube
  • video: a watch page's title, channel, dates, views, likes, caption tracks
  • player: the player's current time, duration, and play state
  • controls: click points for the description expander and Show transcript
  • transcript: the transcript, opening the panel itself when closed
  • like: the like button's state
  • channel: the channel About tab's facts
instagram.com — pxInstagram
  • post: a post or reel page: caption, likes, comments, posted date
  • profile: a profile page: counts, bio, link in bio, contact email
tiktok.com — pxTiktok
  • post: a post's description, duration, and full stats from the page data
  • profile: a creator's identity and counts
x.com — pxX
  • posts: every post on screen in page order
  • thread: the open post and the posts above it
  • profile: the profile header
  • article: an X Article body as HTML
linkedin.com — pxLinkedin
  • profile: the profile you are on
  • experience: the Experience section, newest first
  • education: the Education section
  • job: the open job posting
  • thread: the conversation open in Messaging
reddit.com — pxReddit
  • thread: the open post and the comments rendered now
news.ycombinator.com — pxHackerNews
  • topStories: the front page list with rank, points, author, comment count
zillow.com — pxZillow
  • searchListings: every result card in one read, no scrolling
  • homeFacts: a listing's facts
  • homeSentences: description sentences matching a pattern
  • homePhotos: one full-size URL per distinct photo
  • agentContact: agent email addresses and website link
ebay.com — pxEbay
  • searchCards: search and sold-search result cards
  • soldResearch: the seller sold-research table, waiting for it to settle
  • itemPage: a listing page
booking.com — pxBooking
  • searchCards: one row per property card on a results page
  • searchRecords: each result's id, slug, and coordinates from the page data
  • roomRates: one row per rate in the room table
  • hotelFacts: name, address, coordinates, star class, review count
  • pageState: the stay the page reflects, plus result totals
indeed.com — pxIndeed
  • jobs: every result card on the page in one call
  • posting: the open job's schema.org JobPosting
  • jobDetails: the Job details block: pay, type, shift, benefits
  • applyControls: the apply controls and their state
job-boards.greenhouse.io — pxJobBoardsGreenhouse
  • jobs: one page of up to 50 openings
  • attachments: upload fields that have a file attached
jobs.ashbyhq.com — pxJobsAshbyhq
  • postings: the board's openings
  • posting: the open posting
  • application: the application form's fields and their state
github.com — pxGithub
  • comments: the conversation on an open PR or issue
  • items: one row per issue or PR on a list page
  • repos: one row per repository on a user or org page
  • changedFiles: each changed file on a PR files tab with line counts
  • commits: one row per commit in page order
mail.google.com — pxMailGoogle
  • rows: the Gmail list on screen: inbox, label, or search results
  • compose: the open popup compose window
outlook.office.com — pxOutlookCloud
  • openMessage: the message in the reading pane
  • messageLinks: links in the open message, one per destination
docs.google.com — pxDocsGoogle
  • slideParagraphs: paragraphs of the open slide in reading order
  • slideLines: visible lines of the open slide, top to bottom
  • slideRuns: each text run with its box, fill, and font weight
  • slides: every slide in the deck from the filmstrip
  • sheetTabs: every tab of the open workbook with click points
  • docState: the file open in this tab and the signed-in account
  • docComments: comment cards in the comments pane
drive.google.com — pxDriveGoogle
  • rowPoint: a row's click point, and whether it is rendered
  • selectedRows: the rows currently selected
app.notion.com — pxAppNotion
  • pageBlocks: top-level blocks as typed text, secrets redacted
  • tables: each table block as rows of cell text
  • images: image URLs with rendered width
  • columns: a database view's column names and order
app.slack.com — pxAppSlack
  • messages: the open channel, DM, or thread
  • search: visible hits of an open search
  • profile: the open profile card
  • draft: every composer holding unsent text
  • unread: messages after the unread divider
app.apollo.io — pxAppApollo
  • people: one record per visible row of the People table
  • revealEmail: the access point to click per name, in order
  • companies: one record per visible row of the Companies table
  • companyPage: the open company detail page
  • counter: the pager's count, for detecting a page change
  • settle: resolves once the table has actually changed
chatgpt.com — pxChatgpt
  • thread: the turns of an open chat that are mounted now
  • images: generated renders and uploads in the chat
  • codeBlocks: every code element in the newest reply
*.instructure.com — pxInstructure
  • quizQuestions: each question's number, prompt, and answer controls
  • quizResults: a submitted quiz's score and per-question outcome
  • moduleOutline: a course's modules and their items

Measuring the actual pain point

  • 964 hspent running a script the agent had already written before — the same characters, typed out again from scratch in a new conversation. 134,403 calls, a fifth of all JavaScript time on these sites.↳ addressed by the 26 site toolkits — 100 named readers shipped with the browser
  • 105,144different scripts written to read just 26 websites. Not 105,144 different jobs — the same handful of jobs, spelled 105,144 ways. 98.8% were used in one conversation and deleted.↳ addressed by execute_javascript(toolkit="<site>") — written once, versioned, reused
  • 9,421×One script that pulls the follower count, bio and post count off an Instagram profile. It is the same three fields every time, and it was re-written and re-run 9,421 times. The next three ran 3,197, 1,573 and 1,005 times.↳ addressed by pxInstagram.profile, pxX.posts, pxLinkedin.job and pxGoogle.place
  • 1,660 hof typing querySelector chains at these 26 sites and reading the results back — a third of every JavaScript hour spent there, across 3,571 users.↳ addressed by the 26 toolkits — pxAmazon, pxFacebook, pxYoutube, pxZillow and 22 more
  • 54 hThe script returned []. The agent ran the exact same script again. It returned [] again. 6,381 of 8,555 retries went this way.↳ addressed by the three-anchor contract — every reader returns ok, empty or drift, never a bare [] — section 2.3

Jul 8 – Oct 8, 2026, the 26 head sites from section 1.2: 646,197 JavaScript calls, 4,984 hours, 31,983 runs. Same measurement basis as above.


2.3  Making breakage loud: drift detection and self-healing

A site changes its markup and a hand-written selector returns []. Nothing errors, so the agent treats an empty page as an empty result. Every reader therefore marks its own outcome against three anchors.

So zero rows only counts as empty when the page itself says it is empty. Anything else is reported as breakage. Every call returns the verdict alongside the data:

{ "polar": { "toolkit": "instagram", "fn": "profile", "status": "drift" }, "data": null }

The tool wraps every exported function before the agent's code runs, so the verdict is recorded where the call happens and the agent cannot discard it. On drift the tool hands back the toolkit's own source, numbered, with an instruction to repair it:

✗ Drift: instagram.profile reported status "drift" — its selectors no longer match this page. SELF-HEALING: source is at /home/user/toolkits/instagram.js and below. Do NOT Read it. Edit it to match what this page actually contains (check with read_page first), then call execute_javascript again with toolkit="instagram" — your edited copy is used from then on. 1 | globalThis.pxInstagram = (function () { 2 | "use strict"; ...

This ran for real. In one Instagram lead-generation run the profile reader drifted 98 times, the agent read the page, and made a two-character widening:

- const surface = document.querySelector("article"); + const surface = document.querySelector("article, main[role='main']");

After the edit: 0 drifts and 35 successful calls for the rest of the run. The repair lives in the sandbox for that run only, so it never silently becomes the shipped toolkit.


SECTION 3

A/B testing

I took real user prompts out of the production trace database and replayed each one twice against the same live sites: once on main, once on the branch carrying the six tool changes and the 26 site toolkits. Everything else is the same commit, so the tooling is the only difference, and every figure below comes from Polar's own tables.

Eight pairs where the branch finished with fewer calls, less wall clock, or less money. Delivered is what each agent reported producing, quoted from its own closing answer, so an arm cannot look cheap by quitting early without it showing in that column.

So what

PairPrompt, as the user wrote itDelivered, base → branch What the branch used CallsΔSecsΔCostΔ
google#6Need construction and builder leads. 50 company names in charlottesville50 → 56 companiesharvest_list ×116 → 5−69%88 → 46−48%$0.181 → $0.077−57%
google#107Using Google Maps, find 40 restaurants in Portland, Oregon that have a rating of 4.3 or higher and at least 100 reviews…40 → 40 restaurantsharvest_list ×3
pxGoogle.place ×40
144 → 146+1%464 → 236−49%$0.302 → $0.202−33%
reddit#201Go to reddit.com/r/SaaS/top/?t=month and collect the top 150 posts… title, author, score, comments, permalink150 → 150 postsharvest_list ×15 → 3−40%27 → 20−26%$0.091 → $0.037−59%
tiktok#105Find me 50 creators on instagram and tiktok that post 75 hard lifestyle content and give me ideas - more than 10k followers50 → 51 creatorsget_links ×121 → 17−19%227 → 122−46%$0.226 → $0.220−3%
facebook#109Access my Facebook Marketplace and mine all the Honda City in Mumbai Region… URL, contact number, location, separate by transmission46 → 43 listingswait_for= ×121
harvest_list ×3
pxFacebook.listingDetail ×2
672 → 367−45%661 → 398−40%$0.585 → $0.464−21%
facebook#108Comb Facebook Marketplace for a full suspension mountain bike, frame size XL, with 29" wheels and compile every real listing into a sheet37 → 26 listingsharvest_list ×3
pattern= ×2
wait_for= ×1
pxFacebook.listingDetail ×2
1488 → 492−67%2071 → 601−71%$1.983 → $0.866−56%
google#102Can you open google maps for city Zagreb find all restaurants, hotels with restaurants and vineries. Extract them in Excel…266 → 254 placesget_links ×53
harvest_list ×4
pattern= ×2
pxGoogle.place ×263
1763 → 1432−19%1475 → 1216−18%$1.590 → $0.984−38%
youtube#104go through this youtube channel and go though each and every video… give me the list of all the stocks they are recommended161 → 162 videosharvest_list ×4
wait_for= ×1
pxYoutube.transcript ×283
pxYoutube.video ×226
1936 → 1206−38%1868 → 987−47%$1.998 → $1.748−13%

Each prompt was run more than once per arm; the table shows the most favorable complete pairing, chosen by script. Six delivered as much as base or more. The two that did not are shown with the gap: 26 of 37 bike listings and 254 of 266 Zagreb places.

3.1  Observations and limitations