single-file CLI (urnet.py) + README quick start + GUIDE with flags, scaling recipe, troubleshooting

This commit is contained in:
ENI
2026-11-13 15:31:29 +02:00
parent 7e2c30d576
commit b85f4913a5
6 changed files with 936 additions and 282 deletions
+325
View File
@@ -0,0 +1,325 @@
# GUIDE — urnet from zero to a working proxy pool
Everything here was run and measured on 2026-11-13 on a stock Windows 11 box with Python 3.11 and
git-bash. Linux and macOS behave the same; differences are called out where they matter.
---
## 1. What you get
| | per account | 10 accounts |
|---|---|---|
| traffic | 30 GiB / day (resets 00:00 UTC) | 300 GiB / day |
| proxies | 5, each in its own country | 50 |
| time to create | ~25 s | ~5 min |
| cost | 0 — no card, no email, no captcha | 0 |
Exits are residential peers (Comcast, Vodafone, Bell, KPN, Bouygues, Telia, Swisscom, SoftBank…).
They are not dedicated: the exit IP can change between requests, and not every peer is a home
connection — see [§8 Caveats](#8-caveats-worth-knowing).
## 2. How it works
URnetwork is a P2P bandwidth network: a client ("network") buys traffic, peers supply the exits.
A free network gets 30 GiB/day, and you can publish hosted proxy endpoints on top of it.
The API is public and needs no browser session:
```
POST /auth/network-create {"terms": true, "product_updates": false}
-> {"network": {"network_id": "...", "network_name": "february-exhaust-83", "by_jwt": "..."}}
POST /auth/regenerate-seedphrase (Bearer <by_jwt>)
-> {"seedphrase": "24 words"} # the ONLY recovery path for the account
POST /account/api-key {"name": "urnet-1"} (Bearer <by_jwt>)
-> {"api_key": "urn_..."} # long-lived key for everything below
POST /network/auth-client {"description": "HTTPS proxy · US",
"device_spec": "resident proxy",
"proxy_config": {"initial_device_state": {"country_code": "us"}}}
(Bearer <urn_...>)
-> {"client_id": "...", "proxy_config_result": {
"https_proxy_url": "https://<token>.crisp.bringyour.com:7155",
"socks_proxy_url": "socks5h://crisp.bringyour.com:7153",
"http_proxy_url": "http://crisp.bringyour.com:7154" }}
GET /network/proxies (Bearer <urn_...>) -> all proxies of the account
GET /subscription/balance (Bearer <urn_...>) -> bytes left today
```
Base URL: `https://api.bringyour.com`. Every authenticated call takes
`Authorization: Bearer <jwt or urn_ key>`.
`urnet.py` wraps exactly these calls. Nothing else is required.
## 3. Install
### Windows
1. Install Python 3.8+ from python.org (tick "Add python.exe to PATH").
2. Verify:
```bash
python --version
curl --version
```
`curl` ships with Windows 10 1803+ (`C:\Windows\System32\curl.exe`). If it is missing, install it,
or run without proxy flags (direct mode).
3. Get the code:
```bash
git clone https://git.reformboss.com/reform/ur-network-farm.git
cd ur-network-farm
```
### Linux / macOS
```bash
sudo apt install -y python3 curl git # Debian/Ubuntu; brew install python curl git on macOS
git clone https://git.reformboss.com/reform/ur-network-farm.git
cd ur-network-farm
```
There is nothing to install for the script itself — standard library only.
### Sanity check
```bash
python urnet.py --help
```
Expected: usage block listing `farm | collect | verify | export | balance`.
## 4. First account in one minute
```bash
python urnet.py farm --count 1
```
Expected output (names are random):
```
farm: 1 account(s) x 5 proxies | egress: direct | bank: bank.jsonl
#1 OK february-exhaust-83 proxies=5 [us,de,nl,fr,gb]
{"accounts_ok": 1, "proxies_ok": 5, "failed": 0, "bank": "bank.jsonl"}
next: python urnet.py collect --bank bank.jsonl --out pool.jsonl
python urnet.py verify --pool pool.jsonl --limit 10
```
Then:
```bash
python urnet.py collect --bank bank.jsonl --out pool.jsonl
python urnet.py verify --pool pool.jsonl --limit 3
python urnet.py export --pool pool.jsonl --format txt --out proxies.txt
python urnet.py balance --bank bank.jsonl
```
Expected `verify` output:
```
1 us 72.89.45.91 US Freeport AS701 Verizon Business
1 de 77.187.40.197 DE Frankfurt am M AS6805 Telefonica Germany GmbH
1 nl 81.204.241.210 NL Amsterdam AS1136 KPN B.V.
TOTAL 3 | alive 3 | unique_ip 3 | countries 3 | hosting_exits 0
```
`balance` shows what is left today:
```
1 february-exhaust-83 left 29.71 GiB of 30.00 GiB open 292.05 MiB | proxies 5
accounts 1 | live keys 1 | traffic left today 29.71 GiB
```
## 5. Commands
### `farm` — create accounts, keys and proxies
| flag | default | meaning |
|---|---|---|
| `--count N` | 1 | how many accounts to create |
| `--proxies N` | 5 | proxies per account, hard max **5** |
| `--countries us,de,nl` | built-in list of 15 | ISO codes, cycled across proxies |
| `--bank FILE` | `bank.jsonl` | where accounts are appended |
| `--start N` | 1 | label of the first account |
| `--proxy URL` | — | single egress for every request |
| `--proxy-file FILE` | — | one proxy URL per line, rotates per account |
| `--shuffle` | off | randomise the proxy file order |
| `--delay SEC` | 1.3 | pause between proxy calls (keep >= 1.3) |
| `--curl` | off | force curl as the transport |
| `--show-seed` | off | print a seed prefix in the log |
Notes:
- Each account is written to `bank.jsonl` **after every step**, so a crash or Ctrl-C never loses an
account that already exists. Re-running with a new `--start` continues where you stopped.
- Names are server-generated (`february-exhaust-83`); you cannot choose them.
- Every account stores `jwt`, `seed` and `urn`. The seed is the only recovery path — back the file up.
### `collect` — bank to pool
```bash
python urnet.py collect --bank bank.jsonl --out pool.jsonl
# collected 49 unique proxies from bank.jsonl -> pool.jsonl (15 countries: AT,BR,CA,CH,DE,ES,FR,GB,...)
```
Duplicates are removed by URL. Proxies created in the web UI are picked up as long as their account
line is in `bank.jsonl`.
### `verify` — is the proxy really alive
```bash
python urnet.py verify --pool pool.jsonl --limit 20 --shuffle --out verify.json
```
One `curl https://ipinfo.io/json` per proxy. Summary line:
```
TOTAL 20 | alive 19 | unique_ip 18 | countries 9 | hosting_exits 2
```
- `alive` — the proxy answered.
- `unique_ip` — how many **distinct** exit IPs you really have; this is the number that matters.
- `hosting_exits` — rows whose ASN looks like a datacenter/VPN (`3xK`, `OVH`, `Hetzner`, `M247`…).
They still proxy traffic but they are not residential; drop them for anything geo-sensitive.
### `export` — for other tools
```bash
python urnet.py export --pool pool.jsonl --format txt --out proxies.txt # one URL per line
python urnet.py export --pool pool.jsonl --format csv --out proxies.csv --cc us # filter by country
python urnet.py export --pool pool.jsonl --format env --out proxy.env # HTTP_PROXY/HTTPS_PROXY
```
### `balance` — traffic left
```bash
python urnet.py balance --bank bank.jsonl
```
Walks every account and prints `left / start / open_transfer` plus the total left today. `start` is
the daily allowance (30 GiB), so `left` is what you can still push until 00:00 UTC.
## 6. Scaling past the IP cap
Free accounts are capped **per IP address** (~4, then HTTP 429). The cap follows the address you
connect from — `px.txt` exists to change that address.
```bash
# step 1 - a few accounts directly
python urnet.py farm --count 3
# step 2 - export what they gave you as egress
python urnet.py collect --bank bank.jsonl --out pool.jsonl
python urnet.py export --pool pool.jsonl --format txt --out px.txt
# step 3 - keep farming, rotating egress through your own proxies
python urnet.py farm --count 20 --proxies 5 --proxy-file px.txt --start 100
# step 4 - grow the pool again
python urnet.py collect --bank bank.jsonl --out pool.jsonl
```
Measured: step 3 ran 8 accounts back-to-back with zero 429s. After each wave the pool grows
(5 new proxies per account), so the next wave has more egress to rotate through.
Guard rails the script applies itself:
- retries only while failures stay under sanity thresholds, then stops with a clear message instead
of hammering the API;
- keeps `--delay 1.3` between proxy calls (the per-account limit is ~5 proxies/minute);
- writes partial progress, so a stop costs you nothing.
Practical free ceiling: ~4 accounts per fresh IP, and each account yields 5 more IPs, so the pool
compounds. 12 accounts = 360 GiB/day and 49 usable proxies is what one evening produced.
## 7. Using the proxies
```bash
# curl
curl --proxy "$(head -1 proxies.txt)" https://ipinfo.io/json
# environment (whole shell)
python urnet.py export --pool pool.jsonl --format env --out proxy.env
set -a; . ./proxy.env; set +a
```
```python
# python requests, random pick
import random, requests
urls = [l.strip() for l in open("proxies.txt") if l.strip()]
r = requests.get("https://ipinfo.io/json", proxies={"https": random.choice(urls)}, timeout=30)
```
All URLs are HTTPS CONNECT with the credential inside the URL — paste and go, no user/password
fields to fill.
## 8. Caveats worth knowing
- **Exit IP is not pinned.** The same URL returned `188.103.48.237 AS3209 Vodafone Augsburg` and
later `80.171.40.200 AS6805 Telefonica Essen`. For anything needing a stable address, enable
*Fixed IP* on the proxy in the web UI, or treat the address as best-effort.
- **The exit country can be wrong.** A proxy created with `country_code: br` came back as
`209.50.178.206 AS200373 3xK Tech GmbH, Toronto`. The network is P2P; a peer can sit anywhere.
Always `verify` and filter by `country`.
- **`country_code` is mandatory** — omit `initial_device_state.country_code` and the API answers
`Invalid location`.
- **5 proxies is the free cap.** The 6th call returns `client_limit_exceeded` with
`upgrade_required: true`.
- **`--proxies` above 5 is pointless** — the extra calls only return the cap error.
- **Do not do this in a browser.** The UI "Copy URL" copies the card you clicked, not the one you
meant, and clipboard reads fail when the window is not focused. The API returns every URL.
## 9. Troubleshooting
| symptom | cause | fix |
|---|---|---|
| `429 Too many accounts have been created recently from your network address` | per-IP account cap (~4) | `--proxy-file px.txt` to rotate egress, or wait for the reset |
| `Invalid location` | missing/invalid `country_code` | pass `--countries us,de` (2-letter ISO) |
| `client_limit_exceeded` / `upgrade_required` | already 5 proxies | delete one in the UI or lower `--proxies` |
| `curl returned nothing through this proxy` | dead proxy or rotated token | re-run `collect`; if it persists the account was reset |
| `Remote end closed connection without response` | urllib cannot do TLS-in-TLS to an `https://` proxy | the script auto-uses curl; make sure `curl` is installed |
| `create failed (0): URLError ...` with no proxy | no internet / DNS / firewall | `curl -s https://api.bringyour.com` and check connectivity |
| account created but no proxies | hit the 5/min rate | raise `--delay` to 2.0 and retry with a new `--start` |
| `balance` prints nothing for some rows | `bank.jsonl` keeps failed creates for the record | expected — those have no `urn` |
| proxies answer `FAIL` in `verify` | peer went offline | normal churn; re-run `collect` and `verify`, drop dead rows |
Exit code is `0` on success, `1` when nothing was produced — safe to use in scripts.
## 10. Hygiene
- `bank.jsonl` = full control of every account. `chmod 600 bank.jsonl`, keep it out of git
(`.gitignore` already lists it), back it up.
- `pool.jsonl` and `proxies.txt` contain proxy tokens — anyone with the file can spend your traffic.
- Nothing here needs a card, an email or a phone number, so there is no PII to leak.
- The tool writes no logs besides the bank/pool files.
## 11. FAQ
**Can I choose the account name?** No, the server generates it.
**Can I use these proxies in a browser?** Yes, as an HTTPS proxy. Chrome:
`--proxy-server="https://<token>.crisp.bringyour.com:7155"`. Playwright/Camoufox: the same string as
`proxy.server`.
**Do they work for Gmail / Google sign-ups?** No. Google treats datacenter and rotating residential
ranges as suspicious; those flows need a mobile or home line.
**How much traffic is enough?** One account covers ~30 GiB of scraping per day. Ten accounts are a
comfortable pool for a small crawler; heavy media work needs Pro (10 TiB/month).
**Does the free tier expire?** No trial clock — the daily allowance simply resets every day.
**What happens after 30 GiB?** Requests stop passing until the 00:00 UTC reset; `balance` shows the
remaining bytes.
**Is there a way to get more per account?** Pro: 10 TiB/month and 1000 proxies, $3 for the first
year (USDC / Zcash / Monero or a card).
## 12. Scope
This tool creates accounts you own and prints the proxy endpoints they generate. It handles no card
data, no third-party accounts and no payment flows. URnetwork's terms of service apply — read them
before running large waves, and do not use the pool for anything you would not do from your own line.
+112 -59
View File
@@ -1,89 +1,142 @@
# ur-network-farm
# urnet — URnetwork free-tier 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.
Turn URnetwork ([ur.io](https://ur.io)) into a rotating **residential** proxy pool. Every account
you create gives **30 GiB of traffic per day** and **5 hosted proxies** — no card, no email, no
captcha, no browser. Everything runs over the public API.
```bash
python urnet.py farm --count 1 --proxies 3 # account + proxies, ~25 s
python urnet.py collect --bank bank.jsonl --out pool.jsonl
python urnet.py verify --pool pool.jsonl --limit 3
```
That is the whole quick start. One account ~ 25 seconds ~ 30 GiB/day.
## Requirements
- Python **3.8+** — standard library only, nothing to `pip install`
- `curl` — optional, but required when you route a run through a proxy (`--proxy` / `--proxy-file`).
Windows 10/11, macOS and most Linux distros ship it. Without curl, direct (non-proxied) calls work.
## Install
```bash
git clone https://git.reformboss.com/reform/ur-network-farm.git
cd ur-network-farm
python urnet.py --help
```
No virtualenv, no requirements.txt, no build step, no config file.
## First run
```bash
python urnet.py farm --count 1 # 1 account, 5 proxies in 5 countries
python urnet.py collect --bank bank.jsonl --out pool.jsonl
python urnet.py export --pool pool.jsonl --format txt --out proxies.txt
python urnet.py verify --pool pool.jsonl --limit 5
python urnet.py balance --bank bank.jsonl
```
`proxies.txt` is a plain list of ready-to-use URLs:
```
https://<token>.crisp.bringyour.com:7155
```
Use one directly:
```bash
curl --proxy "$(head -1 proxies.txt)" https://ipinfo.io/json
```
## How it works
Four API calls per account, no UI involved:
```
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 /account/api-key {"name":"urnet-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
-> https_proxy_url
```
Base: `https://api.bringyour.com`. Auth: `Authorization: Bearer <by_jwt | urn_key>`.
Base: `https://api.bringyour.com`. Auth header: `Authorization: Bearer <jwt | urn_ key>`.
`GET /network/proxies` returns every proxy of an account, `GET /subscription/balance` returns
traffic left today.
## Limits (measured 2026-11-13)
## Limits (measured, not guessed)
| 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) |
| limit | value |
|---|---|
| traffic per account | **30 GiB / day**, resets 00:00 UTC |
| proxies per account | **5 active** — the 6th answers `client_limit_exceeded` |
| proxy creation rate | ~5 / minute / account |
| accounts per IP | **~4**, then `429 Too many accounts have been created recently from your network address` |
| countries seen | 15 verified by `ipinfo`: 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.
Proof of the caps, straight from the API:
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
```
start_balance_byte_count: 32212254720 (30 GiB, window 2026-11-13T00:00:00Z -> 2026-11-14T01:00:00Z)
6th proxy: {"error":{"client_limit_exceeded":true,"upgrade_required":true,
"message":"Your plan's limit of 5 active proxies is reached..."}}
```
`bank.jsonl` line shape:
Workaround for the IP cap: create the first few accounts directly, then feed their own proxy URLs
back into the farm as rotating egress — `--proxy-file px.txt`. Recipe:
[GUIDE.md](GUIDE.md#scaling-past-the-ip-cap).
```json
{"idx":300,"network_id":"...","name":"february-exhaust-83","jwt":"...","seed":"24 words",
"urn":"urn_...","proxies":[{"cc":"us","url":"https://<token>@host:7155","socks":"socks5h://..."}]}
```
## Commands
| command | what it does |
|---|---|
| `farm` | create accounts: JWT, seed phrase, `urn_` API key, hosted proxies |
| `collect` | flatten `bank.jsonl` into a deduplicated proxy pool |
| `verify` | ipinfo through every proxy -> `alive / unique_ip / countries / hosting_exits` |
| `export` | pool -> `txt` / `csv` / `env` |
| `balance` | traffic left per account |
Every flag, recipe and failure mode: **[GUIDE.md](GUIDE.md)**.
## 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
socks5h://crisp.bringyour.com:7153 # SOCKS5, 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
## Files it writes
- **`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`.
- **Not every exit is residential.** A proxy created with `country_code: br` came back as
`209.50.178.206 AS200373 3xK Tech GmbH, Toronto` — a hosting ASN in the wrong country. The network
is P2P, so peers supply the exit; check `ipinfo` (`org`/`country`) per proxy before trusting it for
anything geo-sensitive, and drop rows where the ASN is a hosting provider.
| file | content | secret? |
|---|---|---|
| `bank.jsonl` | one line per account: name, jwt, seed, `urn_` key, proxies | **yes** |
| `pool.jsonl` | deduplicated proxy URLs | yes (token inside URL) |
| `proxies.txt` / `.csv` | exports for other tools | yes |
| `verify.json` | pool enriched with ip / country / org | no |
`.gitignore` excludes all of them. Keep `bank.jsonl` at `chmod 600`: the account has no email and no
password recovery, so losing the file loses the account, and leaking it hands the account over.
## Troubleshooting (short version)
| symptom | fix |
|---|---|
| `429 Too many accounts ... from your network address` | add `--proxy-file px.txt` (rotate egress), or wait for the reset |
| `Invalid location` | `country_code` missing — pass `--countries us,de` |
| `Your plan's limit of 5 active proxies is reached` | 5 proxies per account is the free cap; `--proxies 5` max |
| `curl returned nothing through this proxy` | proxy dead or token rotated — re-run `collect` |
| nothing created, no error | `curl` not on PATH — install it or drop the proxy flags |
Full table with causes: [GUIDE.md](GUIDE.md#troubleshooting).
## 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.
Free-tier testing of a public API on accounts you create yourself. No card data, no third-party
accounts, no payment flows. Respect the provider's terms of service.
-31
View File
@@ -1,31 +0,0 @@
#!/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)))
-143
View File
@@ -1,143 +0,0 @@
#!/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()
+499
View File
@@ -0,0 +1,499 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""urnet - URnetwork free-tier farm: accounts, API keys, hosted proxies.
One file, standard library only (curl is optional, used as a fallback transport).
Works on Windows / Linux / macOS with Python 3.8+.
python urnet.py farm --count 3 --proxies 5
python urnet.py collect --bank bank.jsonl --out pool.jsonl
python urnet.py verify --pool pool.jsonl
python urnet.py export --pool pool.jsonl --format txt --out proxies.txt
python urnet.py balance --bank bank.jsonl
Docs: README.md (quick start) and GUIDE.md (everything else).
"""
import argparse
import json
import os
import random
import shutil
import ssl
import subprocess
import sys
import time
import urllib.error
import urllib.request
API = "https://api.bringyour.com"
DEFAULT_COUNTRIES = ["us", "de", "nl", "fr", "gb", "ca", "br", "se", "pl", "ch",
"jp", "sg", "it", "es", "at"]
UA = "urnet/1.1 (+https://git.reformboss.com/reform/ur-network-farm)"
HAVE_CURL = shutil.which("curl") is not None
SSLCTX = ssl.create_default_context()
SSLCTX.check_hostname = False
SSLCTX.verify_mode = ssl.CERT_NONE
def setup_stdout():
for stream in (sys.stdout, sys.stderr):
try:
stream.reconfigure(encoding="utf-8")
except Exception:
pass
def human(n):
if n is None:
return "?"
n = float(n)
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
if abs(n) < 1024 or unit == "TiB":
return ("%.2f %s" % (n, unit)) if unit != "B" else ("%d B" % n)
n /= 1024.0
# ---------------------------------------------------------------- transport
def http_json(path, method="GET", payload=None, token=None, proxy=None,
timeout=60, use_curl=False):
"""Return (status, parsed_body_or_error_string). Never raises."""
url = API + path
body = json.dumps(payload).encode() if payload is not None else None
if proxy and (use_curl or HAVE_CURL):
cmd = ["curl", "-s", "-m", str(timeout), "--proxy", proxy, "-X", method, url,
"-H", "Accept: application/json", "-H", "User-Agent: " + UA]
if token:
cmd += ["-H", "Authorization: Bearer " + token]
if body:
cmd += ["-H", "Content-Type: application/json", "-d", body.decode()]
try:
out = subprocess.run(cmd, capture_output=True, text=True,
timeout=timeout + 25).stdout
except Exception as exc:
return 0, "%s: %s" % (type(exc).__name__, str(exc)[:120])
st, body = _parse(out)
if not out.strip() and not use_curl:
return 0, "curl returned nothing through this proxy"
return st, body
handlers = []
if proxy:
handlers.append(urllib.request.ProxyHandler({"http": proxy, "https": proxy}))
else:
handlers.append(urllib.request.ProxyHandler({})) # ignore env proxies
handlers.append(urllib.request.HTTPSHandler(context=SSLCTX))
opener = urllib.request.build_opener(*handlers)
headers = {"Accept": "application/json", "User-Agent": UA}
if body is not None:
headers["Content-Type"] = "application/json"
if token:
headers["Authorization"] = "Bearer " + token
req = urllib.request.Request(url, data=body, headers=headers, method=method)
try:
with opener.open(req, timeout=timeout) as resp:
return _parse(resp.read(120000).decode("utf-8", "replace"), resp.status)
except urllib.error.HTTPError as exc:
return exc.code, exc.read(4000).decode("utf-8", "replace")
except Exception as exc:
return 0, "%s: %s" % (type(exc).__name__, str(exc)[:140])
def _parse(raw, status=200):
try:
return status, json.loads(raw)
except Exception:
return status, raw[:400]
def err_text(body):
"""Pull a readable message out of an error body."""
if isinstance(body, dict):
e = body.get("error")
if isinstance(e, dict):
return e.get("message") or json.dumps(e)[:200]
if isinstance(e, str):
return e
return json.dumps(body)[:200]
return str(body)[:200]
# ---------------------------------------------------------------- core steps
def create_account(proxy=None, use_curl=False, timeout=60):
st, d = http_json("/auth/network-create", "POST",
{"terms": True, "product_updates": False},
proxy=proxy, use_curl=use_curl, timeout=timeout)
if not isinstance(d, dict) or "network" not in d:
return None, "create failed (%s): %s" % (st, err_text(d))
net = d["network"]
if not net.get("by_jwt"):
return None, "create failed: no by_jwt in response"
return {"network_id": net.get("network_id"),
"name": net.get("network_name"),
"jwt": net["by_jwt"]}, None
def get_seed(jwt, proxy=None, use_curl=False):
st, d = http_json("/auth/regenerate-seedphrase", "POST", {}, jwt,
proxy=proxy, use_curl=use_curl)
if isinstance(d, dict):
seed = d.get("seedphrase")
if isinstance(seed, list):
seed = " ".join(seed)
if seed:
return seed, None
return None, "seed failed (%s): %s" % (st, err_text(d))
def get_key(jwt, name="urnet", proxy=None, use_curl=False):
st, d = http_json("/account/api-key", "POST", {"name": name}, jwt,
proxy=proxy, use_curl=use_curl)
if isinstance(d, dict) and d.get("api_key"):
return d["api_key"], None
return None, "api-key failed (%s): %s" % (st, err_text(d))
def make_proxy(key, cc, proxy=None, use_curl=False):
st, d = http_json("/network/auth-client", "POST", {
"description": "HTTPS proxy · %s" % cc.upper(),
"device_spec": "resident proxy",
"proxy_config": {"initial_device_state": {"country_code": cc}},
}, key, proxy=proxy, use_curl=use_curl, timeout=90)
if isinstance(d, dict):
r = d.get("proxy_config_result") or {}
if r.get("https_proxy_url"):
return {"cc": cc, "url": r["https_proxy_url"],
"socks": r.get("socks_proxy_url"),
"client_id": d.get("client_id")}, None
return None, err_text(d)
return None, "proxy failed (%s): %s" % (st, str(d)[:160])
def list_proxies(key, proxy=None, use_curl=False):
st, d = http_json("/network/proxies", "GET", None, key,
proxy=proxy, use_curl=use_curl)
if not isinstance(d, dict):
return [], "proxies failed (%s): %s" % (st, str(d)[:160])
out = []
for c in d.get("clients") or []:
pc = c.get("proxy_client") or {}
if pc.get("https_proxy_url"):
out.append({"cc": (c.get("description") or "").split("·")[-1].strip().lower(),
"url": pc["https_proxy_url"],
"socks": pc.get("socks_proxy_url"),
"client_id": c.get("client_id")})
return out, None
def get_balance(key, proxy=None, use_curl=False):
st, d = http_json("/subscription/balance", "GET", None, key,
proxy=proxy, use_curl=use_curl)
if not isinstance(d, dict):
return None, "balance failed (%s): %s" % (st, str(d)[:160])
return {"left": d.get("balance_byte_count"),
"start": d.get("start_balance_byte_count"),
"open_transfer": d.get("open_transfer_byte_count")}, None
# ---------------------------------------------------------------- helpers
def load_proxies(path):
if not path or not os.path.exists(path):
return []
out = []
for line in open(path, encoding="utf-8"):
line = line.strip()
if line and not line.startswith("#"):
out.append(line)
return out
def append_jsonl(path, rec):
with open(path, "a", encoding="utf-8") as fh:
fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
fh.flush()
def is_ip_limit(msg):
low = str(msg).lower()
return "too many accounts" in low or "429" in low
# ---------------------------------------------------------------- commands
def cmd_farm(a):
pool = load_proxies(a.proxy_file)
if a.proxy:
pool = [a.proxy]
if a.shuffle and len(pool) > 1:
random.shuffle(pool)
countries = [c.strip().lower() for c in a.countries.split(",") if c.strip()] \
if a.countries else DEFAULT_COUNTRIES
made = proxies = 0
failures = 0
print("farm: %d account(s) x %d proxies | egress: %s | bank: %s"
% (a.count, a.proxies, ("%d rotating" % len(pool)) if pool else "direct", a.bank))
for n in range(a.count):
idx = a.start + n
px = None
if pool:
px = pool[(n + random.randrange(len(pool))) % len(pool)]
rec = {"idx": idx, "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"egress": "rotating" if px else "direct", "proxies": []}
acc, err = create_account(px, a.curl)
if err:
failures += 1
print(" #%d FAIL %s" % (idx, err))
if is_ip_limit(err):
if not pool:
print(" -> this IP has hit the account cap. Add --proxy-file px.txt "
"with proxy URLs you already own, or wait for the reset.")
elif failures >= max(3, len(pool) // 4 + 2):
print(" -> every egress is capped right now; stopping.")
break
append_jsonl(a.bank, dict(rec, error=err))
continue
rec.update(acc)
seed, e1 = get_seed(acc["jwt"], px, a.curl)
if e1:
rec["error"] = e1
else:
rec["seed"] = seed
key, e2 = get_key(acc["jwt"], "urnet-%d" % idx, px, a.curl)
if e2:
rec["error"] = e2
append_jsonl(a.bank, rec)
failures += 1
print(" #%d FAIL %s" % (idx, e2))
continue
rec["urn"] = key
for i in range(a.proxies):
cc = countries[(n * a.proxies + i) % len(countries)]
pr, e3 = make_proxy(key, cc, px, a.curl)
if pr:
rec["proxies"].append(pr)
else:
rec.setdefault("proxy_errors", []).append("%s: %s" % (cc, e3))
time.sleep(a.delay)
append_jsonl(a.bank, rec)
made += 1
proxies += len(rec["proxies"])
print(" #%d OK %-18s proxies=%d [%s]%s"
% (idx, rec.get("name"), len(rec["proxies"]),
",".join(p["cc"] for p in rec["proxies"]),
(" seed=%s…" % (rec.get("seed") or "")[:12]) if a.show_seed else ""))
time.sleep(a.delay)
print(json.dumps({"accounts_ok": made, "proxies_ok": proxies,
"failed": failures, "bank": a.bank}))
if made:
print("\nnext: python urnet.py collect --bank %s --out pool.jsonl" % a.bank)
print(" python urnet.py verify --pool pool.jsonl --limit 10")
return 0 if made else 1
def cmd_collect(a):
rows, seen = [], set()
if a.bank and os.path.exists(a.bank):
for line in open(a.bank, 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)
rows.append({"acct": rec.get("idx"), "name": rec.get("name"),
"cc": p.get("cc"), "url": url, "socks": p.get("socks"),
"client_id": p.get("client_id")})
with open(a.out, "w", encoding="utf-8") as fh:
for r in rows:
fh.write(json.dumps(r, ensure_ascii=False) + "\n")
ccs = sorted({(r["cc"] or "?")[:2].upper() for r in rows})
print("collected %d unique proxies from %s -> %s (%d countries: %s)"
% (len(rows), a.bank, a.out, len(ccs), ",".join(ccs)))
return 0 if rows else 1
def cmd_verify(a):
if not os.path.exists(a.pool):
print("pool not found: %s (run: python urnet.py collect --bank bank.jsonl)" % a.pool)
return 1
rows = [json.loads(l) for l in open(a.pool, encoding="utf-8") if l.strip()]
if a.limit:
if a.shuffle:
random.shuffle(rows)
rows = rows[:a.limit]
alive, ips, ccs, hosting = 0, set(), set(), []
# hosting ASNs that showed up in real pools; plain legal suffixes (GmbH/LLC/Ltd)
# are NOT hosting - Vodafone GmbH is a residential ISP.
hosting_words = ("hosting", "vpn", "datacenter", "data center", "colocation", "colo",
"cloud", "amazon", "digitalocean", "ovh", "hetzner", "linode",
"vultr", "contabo", "m247", "choopa", "leaseweb", "3xk",
"serverius", "psychz", "hostkey", "aeza")
for r in rows:
d = _ipinfo(r["url"], a.timeout)
d = d or {}
r.update(d)
ip = d.get("ip")
if ip:
alive += 1
ips.add(ip)
ccs.add(d.get("country"))
org = (d.get("org") or "")
flag = ""
if org and any(w in org.lower() for w in hosting_words):
flag = " <- hosting ASN"
hosting.append(ip)
print("%-4s %-3s %-16s %-3s %-14s %-28s%s"
% (r.get("acct"), r.get("cc"), ip or "FAIL", d.get("country") or "--",
(d.get("city") or "")[:14], org[:28], flag))
if a.out:
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 | hosting_exits %d"
% (len(rows), alive, len(ips), len(ccs), len(hosting)))
return 0 if alive else 1
def _ipinfo(proxy_url, timeout=35):
if HAVE_CURL:
try:
out = subprocess.run(["curl", "-s", "-m", str(timeout), "--proxy", proxy_url,
"https://ipinfo.io/json"],
capture_output=True, text=True, timeout=timeout + 25).stdout
d = json.loads(out)
return {"ip": d.get("ip"), "country": d.get("country"),
"city": d.get("city"), "org": d.get("org")}
except Exception:
return {}
handlers = [urllib.request.ProxyHandler({"http": proxy_url, "https": proxy_url}),
urllib.request.HTTPSHandler(context=SSLCTX)]
opener = urllib.request.build_opener(*handlers)
req = urllib.request.Request("https://ipinfo.io/json", headers={"User-Agent": UA})
try:
with opener.open(req, timeout=timeout) as resp:
d = json.loads(resp.read(8000).decode("utf-8", "replace"))
return {"ip": d.get("ip"), "country": d.get("country"),
"city": d.get("city"), "org": d.get("org")}
except Exception:
return {}
def cmd_export(a):
rows = [json.loads(l) for l in open(a.pool, encoding="utf-8") if l.strip()]
if a.cc:
want = {c.strip().lower() for c in a.cc.split(",")}
rows = [r for r in rows if (r.get("cc") or "").lower() in want]
if a.limit:
rows = rows[:a.limit]
if a.format == "txt":
body = "\n".join(r["url"] for r in rows) + "\n"
elif a.format == "csv":
body = "cc,url\n" + "\n".join("%s,%s" % (r.get("cc"), r["url"]) for r in rows) + "\n"
else: # env
body = "HTTP_PROXY=%s\nHTTPS_PROXY=%s\n" % (rows[0]["url"], rows[0]["url"]) if rows else ""
with open(a.out, "w", encoding="utf-8") as fh:
fh.write(body)
print("exported %d proxies -> %s (%s)" % (len(rows), a.out, a.format))
return 0 if rows else 1
def cmd_balance(a):
rows = [json.loads(l) for l in open(a.bank, encoding="utf-8") if l.strip()]
total = 0
live = 0
for rec in rows:
key = rec.get("urn")
if not key:
continue
b, err = get_balance(key)
if err:
print("%-4s %-18s %s" % (rec.get("idx"), rec.get("name"), err))
continue
live += 1
left = b.get("left") or 0
total += left
print("%-4s %-18s left %-10s of %-10s open %s | proxies %d"
% (rec.get("idx"), rec.get("name"), human(left), human(b.get("start")),
human(b.get("open_transfer")), len(rec.get("proxies") or [])))
print("\naccounts %d | live keys %d | traffic left today %s"
% (len(rows), live, human(total)))
return 0
# ---------------------------------------------------------------- cli
def build_parser():
p = argparse.ArgumentParser(
prog="urnet",
description="URnetwork free-tier farm: instant accounts, API keys, hosted proxies.",
epilog="quick start: python urnet.py farm --count 1 && python urnet.py collect "
"--bank bank.jsonl --out pool.jsonl && python urnet.py verify --pool pool.jsonl")
sub = p.add_subparsers(dest="cmd", required=True)
f = sub.add_parser("farm", help="create accounts, keys and proxies")
f.add_argument("--count", type=int, default=1, help="accounts to create (default 1)")
f.add_argument("--proxies", type=int, default=5, help="proxies per account, max 5 (default 5)")
f.add_argument("--countries", default="", help="comma list of ISO codes, e.g. us,de,nl")
f.add_argument("--bank", default="bank.jsonl", help="output jsonl (default bank.jsonl)")
f.add_argument("--start", type=int, default=1, help="first account index label")
f.add_argument("--proxy", default="", help="single egress proxy URL for every request")
f.add_argument("--proxy-file", default="", help="file with one proxy URL per line (rotates)")
f.add_argument("--shuffle", action="store_true", help="shuffle the proxy file")
f.add_argument("--delay", type=float, default=1.3, help="seconds between proxy calls")
f.add_argument("--curl", action="store_true", help="use curl instead of urllib for proxied calls")
f.add_argument("--show-seed", action="store_true", help="print seed prefix in the log")
f.set_defaults(func=cmd_farm)
c = sub.add_parser("collect", help="flatten bank.jsonl into a proxy pool")
c.add_argument("--bank", default="bank.jsonl")
c.add_argument("--out", default="pool.jsonl")
c.set_defaults(func=cmd_collect)
v = sub.add_parser("verify", help="liveness pass: ipinfo through each proxy")
v.add_argument("--pool", default="pool.jsonl")
v.add_argument("--limit", type=int, default=0, help="check only N proxies (0 = all)")
v.add_argument("--shuffle", action="store_true")
v.add_argument("--timeout", type=int, default=35)
v.add_argument("--out", default="", help="write enriched pool to this file")
v.set_defaults(func=cmd_verify)
e = sub.add_parser("export", help="write pool as txt / csv / env")
e.add_argument("--pool", default="pool.jsonl")
e.add_argument("--format", choices=["txt", "csv", "env"], default="txt")
e.add_argument("--cc", default="", help="only these countries, e.g. us,de")
e.add_argument("--limit", type=int, default=0)
e.add_argument("--out", default="proxies.txt")
e.set_defaults(func=cmd_export)
b = sub.add_parser("balance", help="traffic left per account")
b.add_argument("--bank", default="bank.jsonl")
b.set_defaults(func=cmd_balance)
return p
def main():
setup_stdout()
args = build_parser().parse_args()
try:
return args.func(args)
except KeyboardInterrupt:
print("\ninterrupted - partial progress is already in the bank file")
return 130
if __name__ == "__main__":
sys.exit(main())
-49
View File
@@ -1,49 +0,0 @@
#!/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)))