<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Aethyn.io Publication]]></title><description><![CDATA[Aethyn.io Publication]]></description><link>https://aethyn-io.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Aethyn.io Publication</title><link>https://aethyn-io.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 09 Sep 2026 03:51:42 GMT</lastBuildDate><atom:link href="https://aethyn-io.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[One Identity Per Task: Keeping an AI Agent's Browser Session Coherent]]></title><description><![CDATA[Most proxy advice was written for scrapers, and if you copy it into an agent you will break your agent in a way that's hard to debug.
The advice goes: rotate your IP on every request. For a scraper, t]]></description><link>https://aethyn-io.hashnode.dev/one-identity-per-task-keeping-an-ai-agent-s-browser-session-coherent</link><guid isPermaLink="true">https://aethyn-io.hashnode.dev/one-identity-per-task-keeping-an-ai-agent-s-browser-session-coherent</guid><category><![CDATA[AI]]></category><category><![CDATA[web scraping]]></category><category><![CDATA[playwright]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[open source]]></category><dc:creator><![CDATA[Aethyn Team]]></dc:creator><pubDate>Sun, 12 Jul 2026 12:50:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a50ec2bc0148d75a6bc3a6e/152fcd65-e904-4f4d-af98-d0ffee2a02db.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most proxy advice was written for scrapers, and if you copy it into an agent you will break your agent in a way that's hard to debug.</p>
<p>The advice goes: <em>rotate your IP on every request.</em> For a scraper, that's right. A scraper pulls a thousand independent pages; there's no relationship between page 1 and page 900, so a fresh exit each time spreads load and dodges per-IP rate limits. Nothing is lost, because there was nothing to preserve.</p>
<p>An agent isn't doing that. An agent runs a <strong>flow</strong>: land on a listing, snapshot the page, click through to page two, read, paginate, extract, compare. Those steps are related. They're supposed to look like one continuous person.</p>
<p>Now rotate the IP between step two and step three. From the site's side, someone arrived from Chicago, and three seconds later the <em>same session</em> is acting from Frankfurt. That is not a bot signature — it's a <strong>hijacked-account signature</strong>, and it's treated more harshly than bot traffic. Best case you get bounced back to a re-auth. Worst case the flow just falls apart, and your agent spends its turns confused about why the page it expected isn't there.</p>
<p>So the requirement for agents inverts: <strong>one stable identity for the length of a task, then a fresh one for the next task.</strong></p>
<p>This post is about how I wired that into an MCP server so the <em>agent itself</em> controls it — <a href="https://github.com/aethynio/aethyn-browser-mcp"><code>aethyn-browser-mcp</code></a>, open source, MIT.</p>
<h2>Sticky identity as a tool argument</h2>
<p>The whole mechanism lives in one argument on one tool.</p>
<pre><code class="language-json">{
  "tool": "aethyn_launch_browser",
  "args": {
    "country": "gb",
    "session": "catalog_walk_01",
    "lifetime_min": 20,
    "tier": "premium"
  }
}
</code></pre>
<p><code>session</code> is a <strong>task id you choose</strong> (letters, digits, underscores). Reuse the same value and you keep the same exit IP. Change it and you get a fresh identity. That's it — that's the sticky mechanism, and it's deliberately something the agent can reason about rather than a config file it can't see.</p>
<p><code>lifetime_min</code> is how long the exit stays pinned: <strong>1 to 1440 minutes, default 10.</strong> Size it to your task, not to your patience — a 20-minute catalog walk gets 20, not 1440.</p>
<p>Two naming things that will save you a debugging session:</p>
<ul>
<li><p>The token is <code>lifetime</code>, not <code>ttl</code>. If you've read a proxy doc that says <code>ttl</code>, that's a different product's grammar.</p>
</li>
<li><p><code>session</code> is what you <em>pass in</em>; <code>session_id</code> is what launch <em>returns</em>, and it's what every other tool takes. Same value, different roles. Keep them straight in your prompts.</p>
</li>
</ul>
<h2>A worked example: walking a public catalog</h2>
<p>Let's do something a scraper-style rotation would wreck: paginate through a public listing, holding one identity the whole way.</p>
<p><strong>Launch the task.</strong> One session id for the entire walk:</p>
<pre><code class="language-json">{ "tool": "aethyn_launch_browser",
  "args": { "country": "gb", "session": "catalog_walk_01", "lifetime_min": 20 } }
</code></pre>
<p><strong>Verify where you're standing.</strong> Before trusting a single localized number on the page:</p>
<pre><code class="language-json">{ "tool": "aethyn_check_exit_ip", "args": { "session_id": "..." } }
→ { "ip": "…", "country": "GB", "city": "…", "org": "…", "is_residential": true }
</code></pre>
<p>This runs <strong>through the session's own proxy</strong>, so it reports where the page will actually be served to — not where your machine is. (<code>is_residential</code> is a best-effort read of the ASN/org, so treat it as a signal rather than a promise.)</p>
<p><strong>Navigate, then snapshot instead of guessing selectors.</strong> This is the part I like most about the MCP ergonomics:</p>
<pre><code class="language-json">{ "tool": "aethyn_navigate", "args": { "session_id": "...", "url": "https://example-catalog.com/listings" } }
{ "tool": "aethyn_snapshot",  "args": { "session_id": "..." } }
</code></pre>
<p><code>aethyn_snapshot</code> returns the <strong>accessibility tree</strong> with <code>[ref=eNN]</code> handles. The model reads roles and names — "link: Next page <code>[ref=e42]</code>" — and clicks the ref:</p>
<pre><code class="language-json">{ "tool": "aethyn_click", "args": { "session_id": "...", "ref": "e42" } }
</code></pre>
<p>No brittle CSS selectors invented by a language model that has never seen the DOM. It reads what's actually on the page, then acts on a handle. (<code>aethyn_click</code> and <code>aethyn_type</code> both accept a <code>selector</code> too, if you already know the page.)</p>
<p><strong>Read, paginate, repeat</strong> — with <code>aethyn_get_content</code> (<code>format: "markdown"</code> keeps the context window clean) — all on the same <code>session_id</code>, which means the same exit IP for the whole walk. To the site, that's one visitor reading a catalog, which is exactly what it is.</p>
<p><strong>Close when the task ends.</strong></p>
<pre><code class="language-json">{ "tool": "aethyn_close", "args": { "session_id": "..." } }
</code></pre>
<p>The next task gets its own <code>session</code> — a fresh identity, no cross-contamination, no shared cookie jar.</p>
<h2>Rotating <em>deliberately</em>, not reflexively</h2>
<p>Sometimes a page comes back wrong. Not an error — wrong. Stripped content, a challenge interstitial, a suspiciously empty body behind a perfectly healthy <code>200</code>.</p>
<p>The agent's move here is <code>aethyn_new_identity</code>:</p>
<pre><code class="language-json">{ "tool": "aethyn_new_identity", "args": { "session_id": "..." } }
</code></pre>
<p>Fresh exit IP in the same country, cookies cleared, session continues.</p>
<p>Now the design decision I want to defend, because it's the kind of thing people ask about:</p>
<p><strong>The tool does not auto-detect blocks.</strong> It doesn't sniff for challenge markers and silently rotate behind your back. The agent reads the content with <code>aethyn_get_content</code>, <em>judges</em> that it's been soft-blocked or served something bogus, and <em>chooses</em> to rotate.</p>
<p>That's more work. It's also the right call. An agent that silently retries under the hood is an agent whose failure modes you cannot see in the trace. If the plumbing quietly swaps identities every time a page looks odd, you lose the one signal telling you the site doesn't want this traffic — and you learn nothing until you're rate-limited across the board. Make the judgement a step the model takes, in the open, where you can read it back.</p>
<p>The corollary: <strong>validate on content, not status.</strong> A <code>200</code> is not success. Assert that the page contains something only the real page would have before you act on it.</p>
<h2>Two configuration traps</h2>
<p><strong>The 407 that isn't a credentials problem.</strong> <code>AETHYN_DEFAULT_TIER</code> defaults to <code>premium</code> → HTTP port <strong>2099</strong>. Elite is port <strong>5499</strong>. If your account is Elite and you leave the default in place, the browser launches perfectly, then the proxy rejects auth with a <strong>407</strong>. It reads like a bad password; it's a port mismatch. Set <code>AETHYN_DEFAULT_TIER=elite</code> or pass <code>tier: "elite"</code> on the call.</p>
<p>Related: <strong>city and state targeting are Elite-only</strong> (5499). Country targeting works on both tiers.</p>
<p><strong>HTTP only, by design.</strong> Chromium can't authenticate SOCKS5, so the server doesn't pretend to offer it.</p>
<h2>It isn't locked to one provider</h2>
<p>Defaults to Aethyn, but any HTTP proxy works — describe how your provider encodes targeting in the username and it adapts:</p>
<pre><code class="language-bash">PROXY_HOST=...
PROXY_PORT=...
PROXY_USERNAME=...
PROXY_PASSWORD=...
PROXY_USERNAME_TEMPLATE="{username}-country-{country}[-city-{city}]-session-{session}-lifetime-{lifetime}"
</code></pre>
<p><code>[optional]</code> segments drop out when they're empty. Fixed proxy with no geo targeting? The template is just <code>{username}</code>.</p>
<h2>Scope and limits</h2>
<p>Stated plainly, because this space overclaims constantly:</p>
<ul>
<li><p><strong>Public data only</strong> — respect <code>robots.txt</code>, rate limits, and site terms.</p>
</li>
<li><p><strong>No CAPTCHA-solving tool exists here, and none is planned.</strong> A challenge is a signal to back off, not a puzzle to beat.</p>
</li>
<li><p><strong>No login or credential-wall automation.</strong></p>
</li>
<li><p>A residential exit fixes your <strong>network vantage point</strong>. It doesn't launder bad pacing, and it isn't an anti-bot bypass. Sticky identity is one layer — the foundational one, not the whole stack.</p>
</li>
<li><p>The browser runs <strong>locally on your machine</strong>. Nothing is hosted for you.</p>
</li>
</ul>
<h2>Try it</h2>
<pre><code class="language-bash">npx -y aethyn-browser-mcp
</code></pre>
<p>Drop it into your MCP client config, and your agent can hold one identity per task on its next tool call.</p>
<p><a href="https://www.aethyn.io/docs/browser-mcp?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=browser-mcp&amp;utm_content=docs"><strong>Docs &amp; runnable examples</strong></a> · <a href="https://www.aethyn.io/blog/residential-proxy-browser-mcp-for-ai-agents?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=browser-mcp&amp;utm_content=blog"><strong>The full writeup</strong></a> · <a href="https://github.com/aethynio/aethyn-browser-mcp">GitHub</a> · <a href="https://www.npmjs.com/package/aethyn-browser-mcp">npm</a> · <a href="https://glama.ai/mcp/servers/aethynio/aethyn-browser-mcp">Glama</a></p>
<p><em>Built by</em> <a href="https://www.aethyn.io/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=browser-mcp&amp;utm_content=intro"><em>Aethyn</em></a> <em>— residential proxies for engineers and agents.</em> <a href="https://www.aethyn.io/signup?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=browser-mcp&amp;utm_content=cta"><em>Free account, no card.</em></a></p>
]]></content:encoded></item><item><title><![CDATA[Why Your Scraper Works Locally but Dies in Production (and How to Actually Fix It)]]></title><description><![CDATA[Same code, same site — green on your laptop, 403 on AWS. It's almost never your code. It's your network.


You built a scraper. It runs beautifully on your laptop. You deploy it to AWS / GCP / a VPS, ]]></description><link>https://aethyn-io.hashnode.dev/why-your-scraper-works-locally-but-dies-in-production-and-how-to-actually-fix-it</link><guid isPermaLink="true">https://aethyn-io.hashnode.dev/why-your-scraper-works-locally-but-dies-in-production-and-how-to-actually-fix-it</guid><category><![CDATA[web scraping]]></category><category><![CDATA[Python]]></category><category><![CDATA[Devops]]></category><category><![CDATA[backend]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Aethyn Team]]></dc:creator><pubDate>Fri, 10 Jul 2026 13:27:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a50ec2bc0148d75a6bc3a6e/74d6aa15-d790-468b-bf81-4345fa03b0de.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Same code, same site — green on your laptop, 403 on AWS. It's almost never your code. It's your network.</h3>
<img src="https://images.unsplash.com/photo-1506399558188-acca6f8cbf41?w=1600&amp;q=80&amp;auto=format&amp;fit=crop" alt="" style="display:block;margin:0 auto" />

<p>You built a scraper. It runs beautifully on your laptop. You deploy it to AWS / GCP / a VPS, run the identical code, and suddenly everything is 403s, CAPTCHAs, or empty pages. You didn't change a line. This is the single most common scraping surprise there is — and it's almost never your code.</p>
<p>The cause: your laptop has been lying to you</p>
<p>At home, you exit through a residential IP — an address your ISP (Comcast, Jio, BT…) assigned to a real household. Anti-bot systems trust it by default; it looks like a person. In production, you exit through a datacenter IP owned by a hosting ASN (Amazon AWS, Google Cloud, DigitalOcean). Those ranges are public and known, and anti-bot vendors discount or block them on sight — because roughly nobody browses a shopping site from an AWS IP.</p>
<p>Same code. Different network reputation. That's the whole trick.</p>
<p>Prove it to yourself — check the ASN you're actually exiting from, on each machine:</p>
<pre><code class="language-python">import requests
info = requests.get("https://ipinfo.io/json", timeout=10).json()
print(info["ip"], info.get("org"))
# laptop -&gt; "AS7922 Comcast Cable"        (residential, trusted)
# prod   -&gt; "AS16509 Amazon.com, Inc."    (datacenter, flagged)
</code></pre>
<p>If prod prints a hosting provider's ASN, you've found your bug — and it isn't in your parser.</p>
<h3><strong>Four things that change between laptop and prod (in order of impact)</strong></h3>
<ul>
<li><p>IP type — the big one. Residential → datacenter. Fix: route production egress through residential proxies so requests read as real users again.</p>
</li>
<li><p>Velocity. On your laptop you fired one request while debugging. In prod you fire hundreds per second from one IP → rate limits and bans. Fix: pace and rotate.</p>
</li>
<li><p>Geography. Your laptop sits in the target's country; your prod region might be us-east-1 when you actually need German pricing or a UK SERP. You get different — or blocked — content. Fix: geo-targeted exits matched to the data you want.</p>
</li>
<li><p>Retries. A failed request retried immediately from the same IP just digs the hole deeper. Fix: back off, then rotate.</p>
</li>
</ul>
<h3>The fix, as architecture</h3>
<ul>
<li><p>Residential egress in production — per-request rotation for independent pages, a sticky session when a flow needs one identity.</p>
</li>
<li><p>Pace with jittered backoff, not a tight loop.</p>
</li>
<li><p>Geo-match the exit to the content you want.</p>
</li>
<li><p>Validate on content, not status — a <code>200</code> can be a soft-block (a decoy or stripped page).</p>
</li>
</ul>
<p>A minimal production-ready pattern that folds all four in:</p>
<pre><code class="language-python">import requests, time, random

# residential + geo-matched + rotates by default
PROXY = "http://USER-country-us:PASS@proxy.aethyn.io:2099"
proxies = {"http": PROXY, "https": PROXY}
HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
                  "(KHTML, like Gecko) Chrome/150.0 Safari/537.36",
    "Accept-Language": "en-US,en;q=0.9",
}

def fetch(url, positive_token, tries=4):
    for i in range(tries):
        r = requests.get(url, proxies=proxies, headers=HEADERS, timeout=30)
        # content check — NOT just r.status_code == 200
        if r.status_code == 200 and positive_token.lower() in r.text.lower():
            return r.text
        time.sleep(min(30, 2 ** i) + random.random())  # back off; a fresh IP rotates in
    raise RuntimeError(f"blocked after {tries} tries: {url}")

html = fetch("https://example.com/product/123", positive_token="add to cart")
</code></pre>
<h3>The honest caveat</h3>
<p>A residential IP fixes reputation, not everything. If you're still blocked after switching, the next layers are your <strong>TLS/JA3</strong> <strong>fingerprint</strong> (plain <code>requests</code> doesn't negotiate TLS like Chrome — try <code>curl_cffi</code> with <code>impersonate="chrome"</code>, or a headless browser) and your pacing. Proxies are one layer of the stack, not a magic bypass — anyone selling them as a complete anti-bot solution is overselling. (<a href="https://www.aethyn.io/solutions/prevent-ip-blocking-web-scraping?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=works-locally-dies-in-prod">More on preventing IP blocks here</a>.)</p>
<h3>TL;DR</h3>
<p>It works on your laptop because your laptop has a <strong>residential</strong> IP. It dies in prod because your server has a <strong>datacenter</strong> IP that every anti-bot system recognizes on sight. Route production through geo-matched residential egress, pace and rotate, and validate on content — and your "it worked on my machine" scraper works in production too.</p>
<p><em>I work on</em> <a href="https://www.aethyn.io/?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=works-locally-dies-in-prod"><em>Aethyn</em></a> <em>— residential proxies built for engineers: one unified endpoint, country/city/session targeting in the username, transparent per-GB pricing, and a free trial with no card. It's the "residential egress in production" layer from this post.</em> <a href="https://www.aethyn.io/signup?utm_source=hashnode&amp;utm_medium=referral&amp;utm_campaign=works-locally-dies-in-prod"><em>Point a free trial at your prod setup →</em></a></p>
]]></content:encoded></item></channel></rss>