The proxy string your bot should accept: one format, every token
2026-10-03 · Bazyl

Your bot needs one proxy parser, not three. Every string our residential pool hands out
is http://USER-<tokens>:PASS@<gateway>:<port>, with all the targeting on the username,
and the two other formats our API can emit are the same four fields in a different order.
Below is a parser and a builder in Python that take all of them.
The thing most people get wrong is where they look for the targeting. Nothing rides the password here. It never changes, and a parser that expects flags on it reads every string wrong.
Four fields, and only one of them is busy
A string is a host, a port, a username and a password, and the username does all the work. Here is a sticky one with two tokens on it:
http://USER-country-de-session-9f2k41ab-lifetime-60:[email protected]:4243
The host is one of three gateways: eu-residential2.basilproxies.com,
us-residential2.basilproxies.com and ap-residential2.basilproxies.com. Same pool,
three doors. Pick the one nearest the machine running the bot, because the choice is
latency and nothing else. The exit country comes from the token: a server in Frankfurt
exiting from US IPs still wants the eu- door.
The port picks the session model. 4242 rotates, a new IP on every request. 4243 is sticky, the same IP for as long as the session token allows. Nothing else answers.
Both ports speak HTTP, and https:// targets tunnel straight through. There is no SOCKS5
on this pool. Point an HTTP-proxy setting at the gateway and you are done.
The token order is fixed, so parse it as a sequence
Tokens ride the username, hyphen-separated, always in the order country, then city or ASN, then OS, then session, then lifetime. The dashboard generator emits that order every time, and the pool reads strings in that order. Six tokens exist:
-country-de: a two-letter ISO code, lowercase. Leave it off and you get the worldwide pool.-city-newyork: the code exactly as the dashboard's city picker writes it. Needs a country token in front. Can't combine with ASN.-asn-7922: digits only, no "AS". Needs a country token. Can't combine with city.-type-residential-os-windows(ormac,ios,android): the-type-residential-part is mandatory. A bare-os-windowsis ignored.-session-9f2k41ab: eight lowercase letters or digits. The same id keeps the same IP.-lifetime-60: minutes, any value from 1 to 1440. 1,440 minutes is 24 hours, and that is the ceiling.
Sticky is port 4243 plus the last two tokens. Rotating is port 4242 and neither of them. Everything else combines with whichever port you picked.
This is what every slot filled looks like, in the API's default format:
eu-residential2.basilproxies.com:4243:USER-country-de-city-berlin-type-residential-os-android-session-9f2k41ab-lifetime-60:PASS
Drop any token and the rest keep their places. A parser that splits the username on hyphens and walks the pairs reads every string the pool will ever hand out, with one exception it has to know about: the OS token is four segments long, not two.
Three output formats, one set of fields
GET /api/v1/proxies on the account API emits the same line in three
formats, and your users will paste all three at you, so accept all three. The call takes
product, country, session (rotating or sticky), count up to 1,000 and format:
host:port:user:pass, the default:eu-residential2.basilproxies.com:4242:USER-country-us:PASSuser:pass@host:port:USER-country-us:[email protected]:4242url:http://USER-country-us:[email protected]:4242
# 3 sticky US lines in url format; the key stays in an env var
curl "https://api.basilproxies.com/api/v1/proxies?product=residential&country=us&session=sticky&count=3&format=url" \
-H "Authorization: Bearer $BASIL_API_KEY"
The endpoint builds lines from credentials the account already holds. Nothing is created and no data is spent, so a bot can call it on every start. The dashboard generator does the same job by hand, up to 100,000 lines per generation, with the same tokens in the same order. Two sources, one parser.
The parser and the builder
Both functions below know the three hosts, the two ports and the six tokens, and nothing
else. parse accepts any of the three formats and refuses a line that isn't ours.
build validates each token and always emits the fixed order, so parse followed by build
turns a hand-typed string with its tokens out of order into one the pool reads.
import re
GATEWAYS = {
"eu": "eu-residential2.basilproxies.com",
"us": "us-residential2.basilproxies.com",
"ap": "ap-residential2.basilproxies.com",
}
PORTS = {"rotating": 4242, "sticky": 4243} # the port picks the session model
HOSTS = {v: k for k, v in GATEWAYS.items()}
MODES = {v: k for k, v in PORTS.items()}
# Fixed order, and what the pool accepts for each token.
RULES = {
"country": r"[a-z]{2}",
"city": r"[a-z0-9]+", # as the dashboard's city picker writes it
"asn": r"\d+", # digits only, no "AS"
"os": r"windows|mac|ios|android",
"session": r"[a-z0-9]{8}",
"lifetime": r"[1-9]\d{0,3}", # 1 to 1440 minutes
}
ORDER = tuple(RULES)
def build(user, password, gateway="eu", mode="rotating", fmt="url", **tokens):
"""Tokens ride the username in the fixed order; the password carries nothing."""
for key, value in tokens.items():
if key not in RULES or not re.fullmatch(RULES[key], str(value)):
raise ValueError(f"bad token {key}={value}")
if "city" in tokens and "asn" in tokens:
raise ValueError("city and asn can't share one username")
if ("city" in tokens or "asn" in tokens) and "country" not in tokens:
raise ValueError("city and asn need a country token")
if "lifetime" in tokens and int(tokens["lifetime"]) > 1440:
raise ValueError("1440 minutes (24 hours) is the ceiling")
parts = [user] + [
# the -type-residential- segment is mandatory; a bare -os- is ignored
f"type-residential-os-{tokens[k]}" if k == "os" else f"{k}-{tokens[k]}"
for k in ORDER
if k in tokens
]
u, host, port = "-".join(parts), GATEWAYS[gateway], PORTS[mode]
if fmt == "url":
return f"http://{u}:{password}@{host}:{port}"
if fmt == "user:pass@host:port":
return f"{u}:{password}@{host}:{port}"
return f"{host}:{port}:{u}:{password}" # host:port:user:pass, the API default
def parse(line):
"""Accept host:port:user:pass, user:pass@host:port or the url form."""
line = line.strip()
if "://" in line and not line.startswith("http://"):
raise ValueError("the pool speaks HTTP: use http:// or drop the scheme")
line = line.removeprefix("http://")
if line.split(":")[0] in HOSTS: # host:port:user:pass
host, port, user, password = line.split(":", 3)
elif "@" in line: # user:pass@host:port, or the url form minus its scheme
creds, endpoint = line.rsplit("@", 1)
user, password = creds.split(":", 1)
host, port = endpoint.rsplit(":", 1)
else:
raise ValueError(f"not one of our residential endpoints: {line}")
if host not in HOSTS or int(port) not in MODES:
raise ValueError(f"not one of our residential endpoints: {host}:{port}")
base, *segs = user.split("-")
tokens, i = {}, 0
while i < len(segs):
if segs[i : i + 3] == ["type", "residential", "os"] and i + 3 < len(segs):
tokens["os"], i = segs[i + 3], i + 4
elif i + 1 < len(segs) and segs[i] in ORDER:
tokens[segs[i]], i = segs[i + 1], i + 2
else:
raise ValueError(f"unknown token near '{segs[i]}'")
return dict(user=base, password=password, gateway=HOSTS[host], mode=MODES[int(port)], **tokens)
line = "eu-residential2.basilproxies.com:4243:USER-country-de-city-berlin-type-residential-os-android-session-9f2k41ab-lifetime-60:PASS"
p = parse(line)
print(build(**p)) # same string, url form
print(build(**p, fmt="host:port:user:pass") == line) # True: the round trip is exact
print(build("USER", "PASS", "us", "sticky", country="us", session="m2p8zq4r", lifetime=30))
Two details are deliberate. A bare -os-windows parses as an OS token and comes back
out as -type-residential-os-windows, which is what the pool wants. And an unknown
token raises instead of being dropped, because a parser that swallows a typo hides it
from the person who made it.
A 407 in the first minute is not a parse error
New credentials can answer 407 Proxy Authentication Required for about a minute while
the gateway picks them up. The partner API reports it as warmupSeconds: 60. Retry
before you touch anything.
If it keeps answering 407 after that, the usual cause is a token on the password or a string in the retired format, which is closed to new customers and which this parser refuses. Regenerate the string in the dashboard and diff the two. The generator's output is the reference, not your parser's.
What this parser does not do
It does not speak SOCKS5, because the pool doesn't. A socks5:// line is not ours, and
parse raises on it. It does not know the city codes, because they come from the
dashboard's picker and there are too many to ship inside a bot; when a city string
misbehaves, generate one in the dashboard and compare. And it does not read the retired
format: other hosts and other ports raise, by design, so a stale list from a customer's
notes fails loudly instead of timing out.
Generate three lines with the curl above, run them through parse, and build the rest
of the integration from the bot developers page.
Common questions
- Which port is rotating and which is sticky?
- Port 4242 rotates, a new IP on every request. Port 4243 is sticky: add -session- followed by 8 lowercase letters or digits and -lifetime- followed by minutes to the username, and the same id keeps the same IP for up to 1,440 minutes (24 hours).
- Does the password carry any targeting?
- No. Every targeting token rides the username, hyphen-separated, in the fixed order country, city or ASN, OS, session, lifetime. The password never changes.
- Which gateway should my bot use?
- The one nearest the machine running it: eu-, us- or ap-residential2.basilproxies.com. The gateway only decides where traffic enters the network. The exit country comes from the -country- token on the username.
- Can a bot use SOCKS5 on the residential pool?
- No. Both ports speak HTTP, and https:// targets tunnel through. Point an HTTP-proxy setting at the gateway and refuse socks5:// lines in your parser.