From 2b65f7ecec8a678aa00ddf4baef4a3b949c6cec1 Mon Sep 17 00:00:00 2001 From: ENI Date: Fri, 13 Nov 2026 15:23:02 +0200 Subject: [PATCH] URnetwork free-tier farm: API-only accounts, keys, hosted residential proxies + measured limits --- .gitignore | 5 ++ README.md | 85 +++++++++++++++++++++++++++++++ collect.err | 1 + collect.py | 31 ++++++++++++ ur_farm.py | 143 ++++++++++++++++++++++++++++++++++++++++++++++++++++ verify.py | 49 ++++++++++++++++++ 6 files changed, 314 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 collect.err create mode 100644 collect.py create mode 100644 ur_farm.py create mode 100644 verify.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..715d787 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bank.jsonl +pool.jsonl +px.txt +verify.json +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7e2522 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# ur-network-farm + +Free-tier **residential** egress from URnetwork (ur.io) without a card, an email, or a browser. +Everything runs over the public API — an account, an API key and up to five hosted proxies in +about 20 seconds. + +``` +POST /auth/network-create {"terms":true,"product_updates":false} -> network.by_jwt +POST /auth/regenerate-seedphrase -> seedphrase (24 words) +POST /account/api-key {"name":"farm-1"} -> urn_... key +POST /network/auth-client {"device_spec":"resident proxy", + "proxy_config":{"initial_device_state":{"country_code":"us"}}} + -> https_proxy_url (creds inside) +GET /network/proxies -> every proxy of the account +GET /subscription/balance -> bytes left today +``` + +Base: `https://api.bringyour.com`. Auth: `Authorization: Bearer `. + +## Limits (measured 2026-11-13) + +| limit | value | proof | +|---|---|---| +| traffic per account | **30 GiB / day**, resets 00:00 UTC | `start_balance_byte_count: 32212254720`, `start_time 2026-11-13T00:00:00Z → end_time 2026-11-14T01:00:00Z` | +| active proxies | **5 per account** | 6th call → `{"error":{"client_limit_exceeded":true,"upgrade_required":true,"message":"Your plan's limit of 5 active proxies is reached..."}}` | +| proxy creation rate | ~5 / minute / account | 429 on bursts; script sleeps 1.3 s | +| instant accounts per IP | ~4, then 429 | `Too many accounts have been created recently from your network address. This limit is scoped to the address you are connecting from and is shared with everyone` | +| countries | 20+ residential providers | 15 verified by `ipinfo` in one evening (US DE NL FR GB CA BR SE PL CH JP SG IT ES AT) | + +Bypass for the IP cap: create the first ~4 accounts directly, then feed their own proxy URLs back +into the farm as rotating egress (`--proxy-file px.txt`). 8 more accounts went through with zero 429. + +Pro tier ($3 first year, USDC/Zcash/Monero or card) raises it to 10 TiB/month and 1000 proxies. + +## Usage + +```bash +# 5 accounts, 5 proxies each, direct connection +python ur_farm.py --count 5 --proxies 5 --bank bank.jsonl + +# more accounts, rotating egress through proxies we already own +python ur_farm.py --count 20 --proxies 5 --proxy-file px.txt --bank bank.jsonl --start 500 + +# export every proxy URL into a flat pool +python collect.py bank.jsonl > pool.jsonl + +# liveness pass over the pool (ipinfo through each proxy) +python verify.py pool.jsonl --limit 20 +``` + +`bank.jsonl` line shape: + +```json +{"idx":300,"network_id":"...","name":"february-exhaust-83","jwt":"...","seed":"24 words", + "urn":"urn_...","proxies":[{"cc":"us","url":"https://@host:7155","socks":"socks5h://..."}]} +``` + +## Proxy URL shape + +``` +https://<40-char-token>.crisp.bringyour.com:7155 # HTTPS CONNECT, token is the credential +socks5h://crisp.bringyour.com:7153 # SOCKS5 with username/password auth +http://crisp.bringyour.com:7154 # plain HTTP +``` + +`https_proxy_url` carries the token inline, so `curl --proxy "$url"` works as-is. + +## Pitfalls + +- **`country_code` is mandatory** in `initial_device_state` — omit it and the call answers `Invalid location`. +- **Exit IP is not pinned.** The same proxy URL returned `188.103.48.237 AS3209 Vodafone Augsburg` + and later `80.171.40.200 AS6805 Telefonica Essen`. Enable *Fixed IP* in the proxy options when a + stable address matters, or treat stability as a known risk. +- **Save the seed before anything else.** The account has no email and no password recovery; losing + the JWT/seed loses the account. +- **Do not drive the browser for this.** UI proxy creation needs clipboard reads that fail when the + document is not focused, and copies the URL of the wrong card. `GET /network/proxies` returns every + URL directly. +- Proxies are residential but shared: several accounts can land on the same provider + (two `Vodafone Hildesheim` exits were seen), so `unique_ip` is the metric, not `alive`. + +## Scope + +Free-tier testing of a public API on accounts we create ourselves. No card data, no third-party +accounts, no payment flows. Respect the provider's terms — this is a field note, not a licence. \ No newline at end of file diff --git a/collect.err b/collect.err new file mode 100644 index 0000000..04305fc --- /dev/null +++ b/collect.err @@ -0,0 +1 @@ +proxies: 34 (unique) from 34 bank lines diff --git a/collect.py b/collect.py new file mode 100644 index 0000000..4b267de --- /dev/null +++ b/collect.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Flatten a bank.jsonl into a proxy pool: one JSON line per proxy URL. + +Usage: python collect.py [bank.jsonl] > pool.jsonl +""" +import json +import sys + +src = sys.argv[1] if len(sys.argv) > 1 else "bank.jsonl" +seen = set() +out = sys.stdout +n = 0 +for line in open(src, encoding="utf-8"): + line = line.strip() + if not line: + continue + try: + rec = json.loads(line) + except Exception: + continue + for p in rec.get("proxies") or []: + url = p.get("url") + if not url or url in seen: + continue + seen.add(url) + row = {"acct": rec.get("idx"), "name": rec.get("name"), "cc": p.get("cc"), + "url": url, "socks": p.get("socks"), "client_id": p.get("client_id")} + out.write(json.dumps(row, ensure_ascii=False) + "\n") + n += 1 +sys.stderr.write("proxies: %d (unique) from %d bank lines\n" % (n, len(seen))) \ No newline at end of file diff --git a/ur_farm.py b/ur_farm.py new file mode 100644 index 0000000..ae32623 --- /dev/null +++ b/ur_farm.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""URnetwork farm — instant networks, API keys, hosted proxies. No browser. + +POST /auth/network-create {"terms": true} -> by_jwt (an account with no email) +then: seedphrase, urn_ key, N hosted HTTPS proxies by country. + +Usage: python ur_farm.py --count 5 [--proxies 5] [--bank bank.jsonl] +""" +import argparse, json, os, ssl, subprocess, sys, time, urllib.request, urllib.error + +sys.stdout.reconfigure(encoding="utf-8") +BASE = "https://api.bringyour.com" +CTX = ssl.create_default_context() +CTX.check_hostname = False +CTX.verify_mode = ssl.CERT_NONE +CODES = ["us", "de", "nl", "fr", "gb", "ca", "br", "se", "pl", "ch", "jp", "sg", "it", "es", "at"] + + +PROXY = None + + +def call(path, method="GET", payload=None, tok=None, timeout=60): + if PROXY: + return call_curl(path, method, payload, tok, timeout) + data = json.dumps(payload).encode() if payload is not None else None + h = {"Accept": "application/json", "User-Agent": "Mozilla/5.0"} + if data: + h["Content-Type"] = "application/json" + if tok: + h["Authorization"] = "Bearer " + tok + req = urllib.request.Request(BASE + path, data=data, headers=h, method=method) + try: + with urllib.request.urlopen(req, timeout=timeout, context=CTX) as r: + body = r.read(60000).decode("utf-8", "replace") + try: + return r.status, json.loads(body) + except Exception: + return r.status, body + except urllib.error.HTTPError as e: + return e.code, e.read(2000).decode("utf-8", "replace")[:400] + except Exception as e: + return 0, "%s: %s" % (type(e).__name__, str(e)[:120]) + + +def call_curl(path, method, payload, tok, timeout): + cmd = ["curl", "-s", "-m", str(timeout), "--proxy", PROXY, "-X", method, + BASE + path, "-H", "Accept: application/json", + "-H", "User-Agent: Mozilla/5.0"] + if tok: + cmd += ["-H", "Authorization: Bearer " + tok] + if payload is not None: + cmd += ["-H", "Content-Type: application/json", "-d", json.dumps(payload)] + try: + out = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 20).stdout + try: + return 200, json.loads(out) + except Exception: + return 0, out[:400] + except Exception as e: + return 0, "%s: %s" % (type(e).__name__, str(e)[:100]) + + +def one(idx, nproxy, country_offset): + st, d = call("/auth/network-create", "POST", + {"terms": True, "product_updates": False}) + if not isinstance(d, dict) or "network" not in d: + return {"idx": idx, "error": "create %s %s" % (st, str(d)[:160])} + net = d["network"] + jwt = net.get("by_jwt") + if not jwt: + return {"idx": idx, "error": "no jwt"} + rec = {"idx": idx, "network_id": net.get("network_id"), + "name": net.get("network_name"), "jwt": jwt, "proxies": []} + + st, sd = call("/auth/regenerate-seedphrase", "POST", {}, jwt) + seed = sd.get("seedphrase") if isinstance(sd, dict) else None + if isinstance(seed, list): + seed = " ".join(seed) + rec["seed"] = seed + + st, kd = call("/account/api-key", "POST", {"name": "farm-%d" % idx}, jwt) + key = kd.get("api_key") if isinstance(kd, dict) else None + if not key: + rec["error"] = "api-key %s %s" % (st, str(kd)[:120]) + return rec + rec["urn"] = key + + for i in range(nproxy): + cc = CODES[(country_offset + i) % len(CODES)] + st, pd = call("/network/auth-client", "POST", { + "description": "HTTPS proxy · %s" % cc.upper(), + "device_spec": "resident proxy", + "proxy_config": {"initial_device_state": {"country_code": cc}}}, key) + r = (pd or {}).get("proxy_config_result") if isinstance(pd, dict) else None + if r and r.get("https_proxy_url"): + rec["proxies"].append({"cc": cc, "url": r["https_proxy_url"], + "socks": r.get("socks_proxy_url"), + "client_id": pd.get("client_id")}) + else: + err = (pd or {}).get("error") if isinstance(pd, dict) else pd + rec.setdefault("proxy_errors", []).append("%s: %s" % (cc, str(err)[:80])) + time.sleep(1.3) + return rec + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--count", type=int, default=5) + ap.add_argument("--proxies", type=int, default=5) + ap.add_argument("--bank", default="bank.jsonl") + ap.add_argument("--start", type=int, default=100) + ap.add_argument("--proxy", default="", help="http(s) proxy URL for the whole run") + ap.add_argument("--proxy-file", default="", help="file with one proxy URL per line (rotates)") + a = ap.parse_args() + + global PROXY + pool = [] + if a.proxy_file and os.path.exists(a.proxy_file): + pool = [l.strip() for l in open(a.proxy_file, encoding="utf-8") if l.strip()] + made, okp = 0, 0 + for n in range(a.count): + if a.proxy: + PROXY = a.proxy + elif pool: + PROXY = pool[n % len(pool)] + idx = a.start + n + rec = one(idx, a.proxies, n * 3) + with open(a.bank, "a", encoding="utf-8") as fh: + fh.write(json.dumps(rec, ensure_ascii=False) + "\n") + if rec.get("urn"): + made += 1 + okp += len(rec.get("proxies", [])) + print("acct%d %-16s proxies=%d %s" % (idx, rec.get("name"), + len(rec.get("proxies", [])), ",".join(p["cc"] for p in rec.get("proxies", [])))) + else: + print("acct%d FAIL %s" % (idx, rec.get("error"))) + time.sleep(1.5) + print(json.dumps({"accounts_ok": made, "proxies_ok": okp, "bank": a.bank})) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/verify.py b/verify.py new file mode 100644 index 0000000..43f9a12 --- /dev/null +++ b/verify.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Liveness pass over a proxy pool: curl ipinfo through each URL. + +Usage: python verify.py pool.jsonl [--limit N] [--timeout SEC] +Prints one line per proxy and a summary: alive / unique_ip / countries. +""" +import argparse +import json +import subprocess +import sys + +try: + sys.stdout.reconfigure(encoding="utf-8") +except Exception: + pass + +ap = argparse.ArgumentParser() +ap.add_argument("pool") +ap.add_argument("--limit", type=int, default=0, help="0 = all") +ap.add_argument("--timeout", type=int, default=35) +ap.add_argument("--out", default="verify.json") +a = ap.parse_args() + +rows = [json.loads(l) for l in open(a.pool, encoding="utf-8") if l.strip()] +if a.limit: + rows = rows[:a.limit] + +alive, ips, ccs = 0, set(), set() +for r in rows: + out = subprocess.run(["curl", "-s", "-m", str(a.timeout), "--proxy", r["url"], + "https://ipinfo.io/json"], + capture_output=True, text=True, timeout=a.timeout + 25).stdout + try: + d = json.loads(out) + except Exception: + d = {} + r.update({"ip": d.get("ip"), "country": d.get("country"), + "city": d.get("city"), "org": d.get("org")}) + if d.get("ip"): + alive += 1 + ips.add(d["ip"]) + ccs.add(d.get("country")) + print("%-4s %-3s %-16s %-3s %-14s %s" % (r.get("acct"), r.get("cc"), d.get("ip", "FAIL"), + d.get("country", "--"), (d.get("city") or "")[:14], (d.get("org") or "")[:26])) + +json.dump(rows, open(a.out, "w", encoding="utf-8"), ensure_ascii=False, indent=1) +print("\nTOTAL %d | alive %d | unique_ip %d | countries %d %s" + % (len(rows), alive, len(ips), len(ccs), sorted(c for c in ccs if c))) \ No newline at end of file