Your competitors price their SaaS plans differently in Germany than in Australia. Your enterprise tier costs 40% less in Southeast Asia than in the US. Without geo-targeted automation, you would never catch it — and you certainly could not verify it at scale.
Anchor VPN was built precisely for this: purpose-built network infrastructure that gives your browser agents genuine residential IP addresses in any country, without the fingerprint drift that kills generic proxy setups. In this hands-on guide, we will build a multi-region price monitoring agent that scrapes pricing pages from five different countries simultaneously using Python, Playwright, and Pydantic.
What We're Building
A Python agent that:
- Spins up 5 parallel Anchor sessions, each routing through a different country
- Navigates to a target pricing page from each region
- Extracts structured pricing data with Pydantic
- Returns a normalized comparison you can act on
Setup
pip install anchorbrowser playwright pydantic anthropic
playwright install chromium
import os
import asyncio
from anchorbrowser import Anchorbrowser
from pydantic import BaseModel
from playwright.async_api import async_playwright
anchor = Anchorbrowser(api_key=os.environ["ANCHOR_API_KEY"])
Defining the Data Model
Using Pydantic keeps the output consistent regardless of which region's page you scraped:
class RegionalSnapshot(BaseModel):
country_code: str
page_title: str
body_excerpt: str
error: str | None = None
Creating Geo-Targeted Sessions
Each call to anchor.sessions.create() accepts a proxy object. Set country_code to any ISO 3166-1 alpha-2 code and Anchor routes the session through a residential IP in that country:
REGIONS = ["us", "de", "au", "gb", "sg"]
async def fetch_from_region(country_code: str, url: str) -> RegionalSnapshot:
session = anchor.sessions.create(
proxy={"active": True, "country_code": country_code},
timeout={"max_duration": 60, "idle_timeout": 30},
)
try:
async with async_playwright() as pw:
browser = await pw.chromium.connect_over_cdp(session.ws_endpoint)
page = browser.contexts[0].pages[0]
await page.goto(url, wait_until="domcontentloaded")
title = await page.title()
excerpt = (await page.locator("body").inner_text())[:2000]
await browser.close()
return RegionalSnapshot(
country_code=country_code,
page_title=title,
body_excerpt=excerpt,
)
except Exception as exc:
return RegionalSnapshot(
country_code=country_code,
page_title="",
body_excerpt="",
error=str(exc),
)
finally:
anchor.sessions.stop(session.id)
Running All Regions in Parallel
Python's asyncio.gather fires all five sessions at the same time. Each session is fully isolated — separate browser context, separate IP, separate fingerprint:
async def monitor_pricing(competitor_url: str) -> list[RegionalSnapshot]:
tasks = [fetch_from_region(code, competitor_url) for code in REGIONS]
return list(await asyncio.gather(*tasks))
if __name__ == "__main__":
url = "https://example-competitor.com/pricing"
snapshots = asyncio.run(monitor_pricing(url))
for snap in snapshots:
if snap.error:
print(f"[{snap.country_code.upper()}] ERROR: {snap.error}")
else:
print(f"[{snap.country_code.upper()}] {snap.page_title}")
print(snap.body_excerpt[:200])
print()
Parsing Prices with an LLM
Raw page text is not structured. Wire the snapshots into a quick LLM call to extract plan names and prices — and you get a normalized dataset without writing a custom scraper for every site:
from anthropic import Anthropic
import json
client = Anthropic()
def extract_prices(snapshot: RegionalSnapshot) -> dict:
if snapshot.error:
return {"country": snapshot.country_code, "plans": [], "error": snapshot.error}
msg = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{
"role": "user",
"content": (
"Extract all pricing plans and their monthly costs from this text. "
'Return JSON: {"plans": [{"name": str, "monthly_usd": float|null}]}.
'
+ snapshot.body_excerpt
),
}],
)
try:
return {"country": snapshot.country_code, **json.loads(msg.content[0].text)}
except Exception:
return {"country": snapshot.country_code, "plans": [], "raw": msg.content[0].text}
# Usage
snapshots = asyncio.run(monitor_pricing("https://example-competitor.com/pricing"))
price_data = [extract_prices(s) for s in snapshots]
for region in price_data:
print(f"
=== {region['country'].upper()} ===")
for plan in region.get("plans", []):
print(f" {plan['name']}: ${plan['monthly_usd']}/mo")
Production Tips
- Fresh sessions every run. Do not reuse sessions across runs. Stale cookies and cached geoIP signals can drift your results. Create a new session per job.
- Use idle timeouts. The
idle_timeoutparameter (seconds) auto-kills sessions that stall on slow third-party scripts, preventing orphaned sessions from running up costs. - Handle region blocks gracefully. Some sites block entire countries. Wrap each call in a try/except and log errors to
RegionalSnapshot.errorrather than crashing the batch. - Scale to hundreds of regions. Anchor supports up to 5,000 concurrent sessions created asynchronously. Extend
REGIONSto cover every market you care about without changing the architecture. - Combine with OmniConnect. If the pricing page requires a login, OmniConnect handles authentication per region so your agent lands on the correct account-specific pricing view.
What's Next
This pattern — parallel geo-targeted sessions feeding into a structured extraction step — works for any competitive intelligence workflow: feature pages, changelog entries, job listings, availability status. Swap the URL and the Pydantic model and you have a new monitoring agent in minutes.
For the next layer, connect the output to a scheduled job and store results in a database. Our Trigger.dev + Anchor guide covers exactly that.
Get Started
Sign up at anchorbrowser.io, grab your API key, and run your first geo-targeted browser agent. The full proxy configuration reference is at docs.anchorbrowser.io.
Building a geo-intelligence pipeline on Anchor? We would love to hear about it.



