cd /news/ai-agents/building-an-online-grocery-agent-wit… · home › topics › ai-agents › article
[ARTICLE · art-146262] src=scraping.club ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Building an online grocery agent with Browserless

Browserless, a browser-as-a-service founded by Joel Griffith, was used to build a Playwright-based AI agent that logs into Italian grocery chain Esselunga and adds items to a cart, relying on the /chromium/stealth route, an Italian residential proxy, native CAPTCHA solving, and Authenticated Profiles plus the Session API to persist cookies, localStorage, and IndexedDB across browser restarts. The test found the F5 BIG-IP TS cookie appeared only on the stealth endpoint, and the agent typed credentials one character at a time with random 70-220 ms pauses while waiting for the Browserless.captchaAutoSolved event. The author moved from Browserless's free plan to the Prototyping plan during testing because the free tier was too limited.

by read9 min views2 publishedOct 6, 2026
Building an online grocery agent with Browserless
Image: Scraping (auto-discovered)

When building an AI agent to automate an online task, we recognize the value of our local browser in terms of ease of use. We’re already logged in to most of our preferred websites; the browser’s sessions store the useful part of our most recent navigation, and we’re so used to this that we take it for granted. Our IP score is usually golden, and our hardware is legit, so we don’t get blocked on any website.

But when we build an agent, all this disappears: we need a browser-as-a-service to work for us, usually running in a Linux VM inside a data center and using its IP address, which screams red flags from the first request.

Today, for this issue of “Tool of the Week”, we’re using Browserless to build an AI agent that logs in to a grocery website in Italy and adds something to the cart, simulating an online shopping experience.

Last week Joel Griffith, founder of Browserless, wrote a guest post here on what breaks in long-running authenticated sessions. He described two Browserless features for this problem: Authenticated Profiles (a saved copy of cookies, localStorage, and IndexedDB) and the Session API (a browser identity that persists on disk for days).

We’ll also test this feature by closing and reopening the browser and will check if, by using the same profile, we’ll be able to avoid logging in for the following runs.

The setup #

Browserless is a browser-as-a-service: you connect Playwright or Puppeteer to a remote Chromium over a WebSocket, and you configure it with query parameters in the URL. For these tests I used:

  • the /chromium/stealth route (and/chromium as a comparison on the first target)
  • the built-in residential proxy with proxy=residential&proxyCountry=it , so every session exits from an Italian IP
  • solveCaptchas=true ,the native CAPTCHA solver , which detects a CAPTCHA, solves it and injects the token into the page
  • Authenticated Profiles and the Session API for the persistence part

All the code is Python with Playwright, connected over CDP (connect_over_cdp), always reusing browser.contexts[0]. I do this because the Browserless docs say that profiles are applied only to the default context, and the CAPTCHA events are visible only if you reuse the existing page.

I started on the free plan and moved to the Prototyping plan during testing, since the free tier was a bit limited for me.

First target: Esselunga #

The starting idea for this article was to log in to the Esselunga website (one of the most famous grocery chains in Italy) using my credentials.

So I requested the account.esselunga.it/area-utenti/ page both with the Chromium endpoint and the stealth one. While the page was served on both cases, i got the TS* cookie, a signal of the presence of the F5 BIG-IP suite, only with the stealth endpoint, so I completed the test with it.

The script types the email and password one character at a time, with a random of 70 to 220 ms, and waits for the Browserless.captchaAutoSolved event before clicking the button:

endpoint = ws_url("chromium/stealth", timeout=120000, proxy="residential",
                  proxyCountry="it", proxySticky="true", solveCaptchas="true")
browser = await p.chromium.connect_over_cdp(endpoint)
context = browser.contexts[0]
page = context.pages[0]
cdp = await context.new_cdp_session(page)
cdp.on("Browserless.captchaAutoSolved",
       lambda e: solved.set_result(e) if not solved.done() else None)

The native solver worked in 5 runs out of 7, with solve times between 10 and 35 seconds. In the other 2 runs it stayed in the solving state for 75 seconds without producing a token, and the checkbox in the screenshot was still empty. When the token arrived, the login POST went through F5 without problems (POST /area-utenti/loginExt, then a 302).

So, everything worked?

Well, not really in this case. The website asked for a 2FA on my phone, probably because I didn’t set any OS to emulate in the first place. My hypothesis is that the Linux identity triggered the second factor, even if I can’t prove it from the outside. But after the first 2FA request, Esselunga kept asking for it every time, even when I set the Browserless session to emulate macOS (with the emulationOs=macos parameter) and even from my normal browser. It seems that my account, once flagged, stays in the “ask for 2FA” state for a while, regardless of the device you use.

For this, I decided to move on to the second target, Tigros.

Second target: Tigros #

Tigros is another Italian grocery chain, with an online shop at tigros.it/tigros-spesa-online. The stack is completely different from Esselunga:

  • Cloudflare is in front of the site ( server: cloudflare ,cf-ray )
  • the shop is a Vue single-page app that talks to an API under /ebsn/api/
  • the CAPTCHA is reCAPTCHA Enterprise, loaded with render=<sitekey> , which means the invisible, score-based version: no checkbox at all

To be fair, Cloudflare didn’t challenge the stealth session. It didn’t issue any cf_clearance cookie and the /ebsn/api/* calls returned 200 from the first load, while Browserless detected the CAPTCHA as recaptcha-v3-enterprise.

The login is a dropdown panel with email, password and a “Ricordami su questo dispositivo” (remember me) checkbox, checked by default. I used the same typing approach as before, and the first result was surprising.

Since there was a CAPTCHA involved, I used the option solveCaptchas=true , but this led to failure on each try I made (as a side note, this is an issue the Browserless team is already solving).

Then I ran the same stealth session withoutsolveCaptchas, letting the page generate its own token with grecaptcha.enterprise. The login returned 200 with a welcome message, 2 runs out of 2. Good!

Since on reCAPTCHA Enterprise your session gets a score on the server side, and you don’t know how good it was or why, your request eventually fails. We cannot say why the solveCaptchas option didn’t work. But we can surely say that a stealth browser session was good enough to get a valid token without using the solver.

Anyway, we were able to log in! Now let’s see if we’re able to access the website again without doing it.

Saving the login as a profile

After the successful login, I saved the browser state as an Authenticated Profile. It’s one CDP command, and it worked from a normal stealth connection, without creating a profile session first:

saved = await cdp.send("Browserless.saveProfile", {"name": "tigros-stealth"})

To check the profile, I open a new browser with profile=tigros-stealth in the connection URL and ask the application, not the cookie jar, if I’m logged in. Griffith suggests the same thing in his post. On Tigros, the signal I use is this: a logged-in session calls /ebsn/api/cart/view after load, and the header doesn’t show “Accedi/Registrati” anymore.

The profile worked. A new browser, with no login and a different exit IP (the login came from Ancona and the check from Milan), opened the shop as the logged-in user.

Does the session survive over time? #

Browserless gives you two models here, and Griffith described the difference well. A profile is a copy by value of the state at the moment you saved it. A persisted session is one browser identity whose data on disk keeps changing. I tested both side by side. For the session, I created it with the Session API, starting from the same profile:

body = {"ttl": 7 * 24 * 3600 * 1000, "stealth": True, "profile": "tigros-stealth",
        "proxy": {"type": "residential", "country": "it", "sticky": True}}

A ttl of 7 days is the maximum on the Prototyping plan (1 day on free). One detail from the docs: with Playwright you get the state on disk but not the live browser process between connections, because processKeepAlive needs browser.disconnect(), which Playwright doesn’t have. So every reconnection starts a new process that reloads cookies and localStorage from disk.

I checked both models from new connections, each one going through the Italian residential proxy. (I verified that the session also exits from an Italian residential IP: in my last check it was Rome, on Telecom Italia.)

Within this window the site never rotated the token, so the two models stayed identical and I can’t show a difference between them. The only expiry I know is the one written in the JWT (15 days), and I didn’t observe it.

In this particular case, the IP address change didn’t matter on Tigros, but it’s not a golden rule. Sometimes this could lead to a break in the pipeline, so usually it’s better to use a static IP.

Doing the shopping #

The last step is acting as the user, which is the ultimate goal for our agent. Once the profile is loaded into a new browser, the script types “latte” in the header search box and clicks the cart button on the first product.

Before adding to the cart, I needed to confirm my delivery address and select a random available delivery slot.

Everything was smooth, and then I could finally click the add to cart button for my latte, which triggered this internal API call:

POST /ebsn/api/cart/add
{"items":[{"productId":7728410,"quantity":1,"infos":{"accept_alternatives":false}, ...}]}

The cart/view endpoint confirmed one item, 1.25 euros of UHT milk. Then removed it, and we can definitely say that our agent works! I didn’t try following the purchase process but I think the actual process was enough to show the potential of Browserless for the agentic web.

What it costs #

Browserless bills in units: 1 unit per 30 seconds of browser time, 6 units per MB of residential proxy traffic, and 10 units per successful CAPTCHA solve (failed attempts are free). A login-state check on Tigros cost about 8 units. The cart runs were much heavier, because a full grocery page with images, when using a residential proxy, it means burning a lot of MB: all the tests after the plan upgrade, mostly cart exploration, used 914 units. If you plan to run an agent like this every day, blocking images on the pages where you don’t need them is the first thing I would do.

Final considerations #

This small test confirms the progress made by the browser-as-a-service industry in building reliable tools for the agentic web era. We stopped a couple of steps from creating a fully working grocery shopping assistant, just because we didn’t need it at the moment. The targets were not the most protected websites on the web, but this is true of most websites. I hope you enjoyed this small test, and let me know in the comments section which agent you built recently.

── more in #ai-agents 4 stories · sorted by recency
── more on @browserless 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-an-online-g…] indexed:0 read:9min 2026-10-06 · —