A ten-thousand-row scraping job died two hours in, wallet already charged for the first six thousand rows, when a regional storefront started quietly returning empty product grids instead of a clean 403 I could at least catch. A request from a cloud IP gets fingerprinted as automation by anything that matters — pricing pages, ad verification targets, review aggregators — before retry logic even notices something’s wrong. The fix isn’t a longer timeout. It’s a different exit point.
This walkthrough uses 2extract as the residential and mobile proxy layer — pay-as-you-go exits you target from the username, not a vendor portal.
That’s the problem 2extract is actually solving, and it’s worth being precise about what changes and what doesn’t. It’s a residential and mobile proxy network, billed pay-as-you-go, where every targeting decision — country, city, ZIP, carrier, how sticky a session should be — is a parameter you build into a proxy username rather than a checkbox in a vendor portal. That’s a different operating model from the flat “buy a country, get an IP list” products a lot of data teams have already tried and half-abandoned. The same account is also reachable through an MCP (Model Context Protocol) server at mcp.2extract.com, so an agent — Claude, Cursor, Codex, anything that speaks MCP — can check the balance, look up a geo slug, create a proxy, and cap its spend without anyone hand-writing the client shown below.
I found that out the expensive way, twenty minutes into my first real run. I’d copied a proxy username from the dashboard, then pasted -country-us onto the end of it for a quick test — except the copied string already carried a targeting suffix from an earlier session I’d forgotten to clean up. Two suffixes, one string, an instant 407 that read exactly like a wrong password. It wasn’t. It was a syntax problem wearing an authentication error’s clothes, and it’s the first thing this article will save you from repeating.
What 2extract actually is — and the one thing it isn’t
2extract does not scrape websites. It doesn’t parse HTML, doesn’t decide which fields belong in a product catalog, and doesn’t store anything you collect. What it does is sit between your program and the public internet and make each outbound request look like it originated from a real residential — or, on the mobile product, a real carrier — connection, optionally narrowed to a country, state, city, or ZIP you choose.
Every stack built on top of it has the same three layers. The collector — an agent calling MCP tools, or Python code with requests and BeautifulSoup — chooses URLs, parses responses, dedupes, and writes files. The gateway, at proxy.2extract.net:5555, authenticates the request, applies targeting, picks an exit IP, and tunnels the traffic through. The target is whatever site or API actually holds the data.
Mobile proxies push the residential idea further: the exit is an actual carrier IP (T-Mobile US shows up under ISP code 310260 in the docs), priced separately, and targeted with -isp instead of -country — the two targeting families are mutually exclusive on a single username, and the gateway will reject a request that tries to mix them.
None of this is a license to do anything you couldn’t already justify without a proxy. The Acceptable Use Policy rules out unauthorized access, account farming, scalping, spam, and ad fraud in plain terms, and a residential exit doesn’t override robots.txt, a site’s terms of service, or copyright law.
The proxy username is the actual control plane
Most proxy vendors let you pick a country from a dashboard dropdown or set it in an HTTP header. 2extract does neither. After creating a proxy, the dashboard hands you a base username shaped like 2xt-customer-[CLIENT_ID]-proxy-[PROXY_NAME], and that base is immutable for the life of the proxy. Everything situational gets appended afterward as a hyphenated suffix, built fresh by your code on every request.

@dataclass(frozen=True)
class Targeting:
country: str | None = None
state: str | None = None
city: str | None = None
zip_code: str | None = None
asn: str | None = None
isp: str | None = None
…The remaining 23 lines stay in the interactive article so this page remains a written walkthrough rather than a raw Python dump.
What the residential product is actually sold for
| Use case | Why residential matters here | Session strategy |
|---|---|---|
| eCommerce & price monitoring | Stores change currency, tax, and stock by country; a datacenter IP gets a generic or blocked page | Sticky for the length of a category walk |
| Market research | Discussion APIs and open product databases still rate-limit aggressively at volume | Rotating, one exit per batch |
| Geo / localization | Regional prices, SERPs, and ads only render correctly to an exit that actually looks local | Rotating, unique IP per market sample |
| Ad verification & SEO / SERP | Same mechanics underneath, but SERPs are heavily JavaScript-driven | Usually needs a real browser, not this stack |
How a request travels, and what happens the moment it doesn’t

Every request makes the same trip — collector to gateway to target, response back the same path. A residential peer can simply go offline mid-session. A proxy left in Inactive status returns a 407 even against a perfectly correct password. Gateway-level errors surface in an X-2extract-Error or X-Proxy-Error response header — except that header goes dark over HTTPS CONNECT tunneling.
Python example — skim the functions, then copy what you need.
def _gateway_407(self, targeting, exc):
header = self._read_gateway_header_if_needed(targeting)
hint = header or (
"the proxy is Inactive, the password is wrong, or the wallet is empty. "
"Open My Proxies and set Status to Active, then rerun."
)
if header and "inactive" in header.lower():
hint = f"{header}. Open My Proxies and set Status to Active (Actions menu)."
return GatewayError(f"2extract rejected the proxy login (407): {hint}", status_code=407)
Setting up an account — the part no library can do for you
- Create an account and open the dashboard. Unverified accounts carry a $50 lifetime deposit cap.
- Top up the Pay-As-You-Go wallet. The minimum is $10, and residential and mobile traffic bill from the same balance.
- If the job will ever need more than $50 deposited, verify identity first.
- Create an API key scoped to at least
geo:read,proxies:read,proxies:write, andbalance:read. It’s shown exactly once. - Create one residential proxy, and a second dedicated proxy only if mobile ISP targeting is actually needed.
- Set a monthly spend cap on each proxy.
- Copy the base username and password somewhere safe — a
.envfile if you’re driving this with Python, the agent’s own credential store if it’s MCP — never the targeted version with a suffix already attached.
Running the same account through MCP
Everything above assumes a person writing Python. 2extract also ships an MCP server at mcp.2extract.com, exposing that exact same account — balance, proxies, geo catalog, traffic history — as tools any MCP-capable agent can call directly. Nothing about the underlying product changes: it’s still one residential proxy, still billed pay-as-you-go, still targeted through a username suffix. What changes is who assembles that username — the agent, from a plain-language request, instead of you, from the Targeting dataclass above.

fig. 4 — the round trip behind every tool call: agent → MCP server → account, and back
I connected it as a custom MCP server inside an agent’s tool panel and had 19 tools and 19 resources enabled with nothing more than the server URL and an API key — no separate SDK, no client to install.

📷 the 2extract MCP server connected inside an agent’s tool panel — createProxyResource, deactivateProxyResource, deleteProxyResource, and getAccountBalance visible among the 19 enabled tools

fig. 5 — the six steps below, in one continuous session against the same account
Step 1 — balance, in plain language
The simplest sanity check skips the dashboard tab entirely. Asking the agent “What is my 2extract balance?” triggers one tool call — getAccountBalance — and comes back in about 22 seconds with the wallet figure precise to six decimal places, not just the rounded number the UI shows:
JSON example — use this payload as a starting point.
{
"balance_display": "$4.75",
"balance_exact": "$4.746951",
"note": "enough to create a new proxy"
}

📷 getAccountBalance answering a plain-language balance check — the agent surfaces both the rounded and the exact figure
Step 2 — geo lookup, the same targeting table without a docs tab
Fig. 1 above warned that every geographic suffix has to come from 2extract’s own catalog, never guessed — a state name that’s off by one word doesn’t error, it just silently gets ignored as a targeting parameter. searchGeoRegions puts that catalog directly in the agent’s hands. Asking for Germany’s regions returns the exact slugs the proxy username expects:
| Region | Username param |
|---|---|
| Baden-WĂĽrttemberg | badenwurttemberg |
| Bavaria | bavaria |
| North Rhine-Westphalia | northrhinewestphalia |
| Saxony | saxony |
| Thuringia | thuringia |

📷 searchGeoRegions resolving Germany’s states into the exact -state-… slugs the proxy username expects
Notice what the agent volunteered on its own, unprompted: “do not guess them.” That’s the same warning this article gives around fig. 1 — now enforced by the tool call itself instead of a paragraph the reader has to remember.
Step 3 — creating the proxy the lookup was for
With the slug in hand, the same session calls createProxyResource — the tool sitting right next to getAccountBalance in the panel above — with targeting baked into the proxy itself rather than appended per request:
JSON example — use this payload as a starting point.
{
"tool": "createProxyResource",
"arguments": {
"name": "de-baden-agent-demo",
"type": "residential",
"country": "DE",
"state": "badenwurttemberg"
}
}
The response is the same base-username shape described earlier in this article — 2xt-customer-[CLIENT_ID]-proxy-de-baden-agent-demo — except this proxy now carries Baden-Württemberg targeting as its default, so a plain request with no suffix at all still exits from that region.
Step 4 — a spend cap, without leaving the chat
Step 6 of the account-setup checklist above says to cap spend on every proxy from the dashboard. Over MCP that’s a second tool call on the proxy just created, rather than a separate trip to a settings page:
JSON example — use this payload as a starting point.
{
"tool": "setProxyLimit",
"arguments": {"proxy": "de-baden-agent-demo", "monthly_cap_usd": 5}
}
At a $4.75 balance that cap doesn’t do much for a demo account, but it’s the same guardrail every production proxy behind this article’s three pipelines runs under — just set through the agent instead of the dashboard.
I deactivated and deleted this demo proxy right after testing it — deactivateProxyResource and deleteProxyResource sit right next to createProxyResource in the same tools panel from the setup screenshot above — so a five-minute demo doesn’t sit around inflating the account’s proxy count. That’s also why the dashboard in the next step still shows exactly one active proxy, not two.
Step 5 — checked against the dashboard, not instead of it
MCP doesn’t read from a separate ledger. The balance, the proxy that now exists, and its cap all show up identically in 2extract’s own web dashboard, because both surfaces read and write the same account state. Asking the agent for a wider view confirms it: a 30-day traffic-and-spend rollup pulls the same wallet history the dashboard’s usage tab shows, broken down by the day it actually happened:

📷 a 30-day usage rollup pulled through MCP — $0.25 spent, 60.5 MB of traffic, all through one proxy — the same figures the dashboard’s usage tab reports for that account and window
And the dashboard itself, checked separately right after, confirms it — balance, active-proxy count, and 30-day traffic all lining up with what the agent had just reported:

📷 the account’s own dashboard — 60.49 MB over 30 days and one active proxy, matching what the agent had just reported
Balance, active-proxy count, and traffic all line up across both views — $4.75 agent-side versus $4.74 on the dashboard is a rounding difference, not a discrepancy, and one active proxy shows on both because the demo proxy was deleted right after use. Same account, two windows onto it — not a shortcut around the dashboard, a second way to reach it.
Step 6 — one request actually routed through the result
Everything so far provisions a proxy; it doesn’t yet prove a byte moved through it. Closing the loop means one ordinary HTTP request, made the usual way, through the exact username MCP just created:
Code example — copy the snippet, then match it to your project.
username = "2xt-customer-[CLIENT_ID]-proxy-de-baden-agent-demo"
proxies = {"http": f"http://{username}:{password}@proxy.2extract.net:5555",
"https": f"http://{username}:{password}@proxy.2extract.net:5555"}
response = requests.get("https://httpbin.org/ip", proxies=proxies, timeout=30)
print(response.json())
JSON example — use this payload as a starting point.
{"origin": "89.204.xx.xx"}
That IP resolves to a Baden-Württemberg ISP block, confirming that the region MCP looked up, and the proxy MCP created, is the region a request actually exits from — the same exit.* field the Python pipelines below log on every row, for exactly this reason.
Three pipelines, three genuinely different datasets
Everything above ran through an agent calling MCP tools. The rest of this guide is the same account, driven by code instead — three pipelines built on one client, for anyone who wants to own the collection logic rather than hand it to an agent.
Geo and address: proof of exit location
def _rotating_geo_targeting(country: str) -> Targeting:
"""A fresh -session id forces 2extract to assign a new exit IP for this hop."""
return Targeting(country=country, session=uuid.uuid4().hex[:12], time_minutes=1)
def scrape_steam_price(client, *, country, app_id="730", targeting=None):
url = f"https://store.steampowered.com/app/{app_id}/"
response = client.get(url, targeting=targeting or Targeting(country=country),
params={"cc": country}, timeout=45)
…The remaining 4 lines stay in the interactive article so this page remains a written walkthrough rather than a raw Python dump.
A row from an actual checkpoint file:
JSON example — use this payload as a starting point.
{
"row_number": 41,
"collected_at": "2026-08-14T09:12:03Z",
"label": "DE",
"targeting": {"country": "de", "session": "a91fe2c7b1d4"},
"exit": {"ip": "89.204.xx.xx", "city": "Frankfurt", "region": "Hesse"},
"ok": true
}
eCommerce and price monitoring
Python example — skim the functions, then copy what you need.
def parse_price(text):
raw = " ".join(str(text).split())
currency = "GBP" if "£" in raw else "EUR" if "€" in raw else "USD" if "$" in raw else None
match = re.search(r"[\d]+\.[\d]+|[\d]+", raw.replace(",", ""))
value = float(match.group(0)) if match else None
return raw, value, currency
Output lands in data/output/eCommerce.xlsx. Sessions stay sticky for the length of a category walk.
Market research
Pulls from Hacker News through the Algolia API and from Open Food Facts. Its “sentiment” field is an engagement heuristic derived from points and comment counts — not a human-coded label and not an LLM classifier.
From collected rows to a spreadsheet someone will actually open
Every pipeline writes a JSON checkpoint every hundred rows before touching Excel at all — a habit that’s saved a multi-hour run from a crashed openpyxl write more than once. The JSON-to-Excel step deliberately does no new computation; it only formats data that’s already been collected and validated.
How the package is actually organized

fig. 3 — three pipelines share one client; checkpoints guarantee reproducible dataset generation
None of the three pipeline scripts talk to requests directly. They all go through a single ExtractClient that owns authentication, retry behavior, and username construction, and that boundary is the entire reason adding the market research pipeline took an afternoon instead of a week.
Residential, mobile, or datacenter — the real tradeoffs
| Datacenter | Residential | Mobile | |
|---|---|---|---|
| Exit looks like | Cloud ASN (AWS, GCP) | Home ISP connection | Carrier IP |
| Block / CAPTCHA rate | Highest | Low | Lowest |
| Typical cost | Cheapest | Mid | Highest |
| Targeting granularity | Data center location only | Country / state / city / ZIP | Carrier (ISP code) only |
| Best fit | Low-stakes internal jobs | eCommerce, geo, market research | Mobile-specific SERPs, app traffic |
Where this actually breaks
Country targeting is a request sent to the gateway, not a legal guarantee that a specific city lands on every hop — the only trustworthy record of where a request actually exited is the exit.* data that comes back, and it belongs in every row you store. JSON APIs like DummyJSON and Open Food Facts still return a bare 403 without a properly set User-Agent, proxy or not. Matching “the same product” across two catalogs by name and brand is a heuristic, not an identity. And none of this changes the baseline fact that a proxy alters your odds of getting a response, not your obligation to respect what a site’s terms of service actually say.
How this was tested
Every pipeline described above was run end-to-end against a live 2extract account — real wallet balance, real proxy status states, real 407s along the way — not just read from documentation. The MCP walkthrough was run against that same account in the same session; the balance, geo slugs, and traffic figures shown are what the account actually returned, not stand-ins. The full source is on GitHub under an MIT-style layout anyone can clone and rerun against their own account.
Glossary
| Term | Meaning |
|---|---|
| Exit IP | The public address the target website actually sees |
| Gateway | proxy.2extract.net:5555 |
| Base username | 2xt-customer-…-proxy-name, with no targeting suffix appended |
| Sticky session | Same exit IP held across many requests via -session (+ optional -time) |
| 407 | Gateway rejected authentication or proxy status — rarely the password itself |
| PAYG | Wallet billed per traffic used, not an unlimited monthly crawl allowance |
| MCP server | mcp.2extract.com — exposes balance, proxies, and the geo catalog as tools an agent can call |
Questions worth asking before rolling this into production
Does 2extract scrape websites for me?
It doesn’t — it’s a proxy network that routes requests through residential or mobile exit IPs. Parsing, deduplication, and storage stay entirely the collector’s job.
Does 2extract have an MCP server for agents like Claude, Cursor, or Codex?
Yes — mcp.2extract.com exposes the same account (balance, proxies, geo catalog, traffic history) as tools any MCP-capable agent can call directly, as shown in the walkthrough above. It’s the same wallet and the same billing as the Python package; MCP just hands the control plane to the agent instead of a human writing the client.
Why does a request get a 407 even when the password is definitely right?
In practice that almost always means the proxy is set to Inactive in the dashboard, the wallet balance has run out, or the username carries malformed or conflicting targeting parameters.
Can country targeting and mobile ISP targeting live on the same proxy?
They can’t. Geographic targeting and network targeting are mutually exclusive on a single username, and mobile ISP targeting needs its own dedicated proxy credential besides.
Is a separate proxy needed for every country being targeted?
No — one residential proxy covers every supported country. Targeting is a parameter appended per request, not a separate product purchased per market.
Does a residential proxy make scraping any site legally safe?
It doesn’t. A proxy changes what IP a target sees; it has no bearing on robots.txt, a site’s terms of service, or copyright law.
Further reading
- 2extract quick start
- Geo targeting reference
- 407 errors, explained
- 2extract MCP server
- Full source on GitHub
Pricing, account limits, and interface details in this article reflect 2extracts dashboard as of September 2026, captured while writing and testing the code above end-to-end against a live account. Treat this as a technical guide to the architecture and integration pattern rather than a live pricing reference vendors update rates and UI labels independently of when an article like this is published, so confirm current numbers on the dashboard before budgeting a production run.
To run the same MCP or Python setup against a live wallet, start at 2extract.
Questions this article answers
Short answers first. Open a question to read the working note.
What 2extract actually is — and the one thing it isn't?
2extract does not scrape websites. It doesn't parse HTML, doesn't decide which fields belong in a product catalog, and doesn't store anything you collect. What it does is sit between your program and the public internet and make each outbound request look like it originated from a real residential — or, on the mobile product, a real carrier — connection, optionally narrowed to a country, state, city, or ZIP you choose. Every stack built on top of it has the same three layers.
Where this actually breaks?
Country targeting is a request sent to the gateway, not a legal guarantee that a specific city lands on every hop — the only trustworthy record of where a request actually exited is the exit.* data that comes back, and it belongs in every row you store. JSON APIs like DummyJSON and Open Food Facts still return a bare 403 without a properly set User-Agent, proxy or not. Matching "the same product" across two catalogs by name and brand is a heuristic, not an identity.
