check-proxy-tax 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kais / 600me
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,298 @@
1
+ Metadata-Version: 2.1
2
+ Name: check-proxy-tax
3
+ Version: 1.0.0
4
+ Summary: Zero-dependency CLI that audits real proxy and anti-bot cost: usable rate, p50/p95 latency, bytes billed and effective price per 1,000 usable requests.
5
+ Home-page: https://github.com/600me/check-proxy-tax
6
+ Author: Kais / 600me
7
+ License: MIT
8
+ Project-URL: Homepage, https://github.com/600me/check-proxy-tax
9
+ Project-URL: Repository, https://github.com/600me/check-proxy-tax
10
+ Project-URL: Issues, https://github.com/600me/check-proxy-tax/issues
11
+ Project-URL: Benchmark, https://scrape.mybskm.top/?utm_source=cli&utm_content=check-proxy-tax
12
+ Keywords: proxy,scraping,benchmark,http,cost,cli
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ Classifier: Development Status :: 5 - Production/Stable
16
+ Classifier: Environment :: Console
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3 :: Only
22
+ Classifier: Programming Language :: Python :: 3.8
23
+ Classifier: Programming Language :: Python :: 3.9
24
+ Classifier: Programming Language :: Python :: 3.10
25
+ Classifier: Programming Language :: Python :: 3.11
26
+ Classifier: Programming Language :: Python :: 3.12
27
+ Classifier: Topic :: Internet :: WWW/HTTP
28
+ Classifier: Topic :: System :: Networking
29
+ Classifier: Topic :: Utilities
30
+ License-File: LICENSE
31
+
32
+ # check-proxy-tax
33
+
34
+ **Find out what a "cheap" proxy actually costs per 1,000 usable requests.**
35
+
36
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/)
37
+ [![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#install)
38
+ [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
39
+
40
+ Proxy invoices are priced per request, per IP, or per GB. Your scraper is not
41
+ paid in those units. It is paid in responses it can actually use. When an edge
42
+ returns `403`, a request stalls until timeout, or a `200` arrives without the
43
+ product marker, the attempt is still billed. The gap between the advertised
44
+ price and the price of usable traffic is **The 403 Tax**.
45
+
46
+ `check-proxy-tax` measures that gap directly. It fires a small concurrent burst
47
+ at one URL, classifies every response as usable or blocked, and reports:
48
+
49
+ - usable rate and blocked/failed counts;
50
+ - effective cost per 1,000 usable requests;
51
+ - the tax multiplier over the nominal per-1,000 price;
52
+ - p50 / p95 latency and total bytes transferred;
53
+ - status-code and failure-reason breakdown.
54
+
55
+ It is a single core module using only the standard library. No account, no API
56
+ key, no external dependencies, no background service.
57
+
58
+ ## Install
59
+
60
+ Python 3.8 or newer.
61
+
62
+ ```bash
63
+ git clone https://github.com/600me/check-proxy-tax.git
64
+ cd check-proxy-tax
65
+ python3 -m pip install .
66
+ check-proxy-tax --help
67
+ ```
68
+
69
+ You can also run it straight from a checkout without installing:
70
+
71
+ ```bash
72
+ python3 -m check_proxy_tax.cli --url https://example.com --requests 5
73
+ ```
74
+
75
+ The package is PyPI-ready (`python3 -m build` produces a wheel and sdist), but
76
+ installing from source is the supported path until the first PyPI release.
77
+
78
+ ## Quick start
79
+
80
+ ```bash
81
+ check-proxy-tax \
82
+ --url "https://example.com/product/123" \
83
+ --requests 20 \
84
+ --marker "Add to cart" \
85
+ --price-per-1k 0.49
86
+ ```
87
+
88
+ ```text
89
+ check-proxy-tax 0.1.0 - proxy economics audit
90
+
91
+ +---------------------+------------------------------------------+
92
+ | METRIC | VALUE |
93
+ +---------------------+------------------------------------------+
94
+ | Target URL | https://example.com/product/123 |
95
+ | Samples | 20 (concurrency 4, timeout 10s) |
96
+ | Marker | Add to cart |
97
+ | Usable success | 14 / 20 |
98
+ | Blocked / failed | 6 / 20 |
99
+ | Usable rate | 70.0% |
100
+ +---------------------+------------------------------------------+
101
+ | Nominal price / 1k | $0.4900 |
102
+ | Effective cost / 1k | $0.7000 |
103
+ | Tax multiplier | 1.43x |
104
+ +---------------------+------------------------------------------+
105
+ | Latency p50 | 212.4 ms |
106
+ | Latency p95 | 1840.9 ms |
107
+ | Total transferred | 1.18 MB (1,234,567 bytes) |
108
+ | Status codes | 200:14, 403:5, timeout:1 |
109
+ | Blocked reasons | http_403:5, timeout:1 |
110
+ | Verdict | DEGRADED - 6 of 20 requests blocked |
111
+ +---------------------+------------------------------------------+
112
+
113
+ Benchmark & provider matrix: https://scrape.mybskm.top/?utm_source=cli&utm_content=check-proxy-tax
114
+ FTC Disclosure: Benchmark links may include affiliate tracking.
115
+ ```
116
+
117
+ ## What counts as usable
118
+
119
+ A sample is **usable** only when both conditions hold:
120
+
121
+ 1. the final HTTP status is exactly `200`;
122
+ 2. when `--marker` is supplied, the response body contains that string
123
+ (case-sensitive, matched against the raw UTF-8 bytes).
124
+
125
+ Everything else is **blocked**: `403`, `429`, other `4xx`, `5xx`, timeout,
126
+ connection/TLS failure, or a `200` without the marker. Redirects are followed
127
+ and the final status is classified.
128
+
129
+ That definition is deliberately strict. A `200` challenge page, a consent
130
+ screen, or an empty shell is not inventory your scraper can use.
131
+
132
+ ## The math
133
+
134
+ ```text
135
+ usable_pct = usable_count / total_requests * 100
136
+ effective_cost_per_1k = nominal_price_per_1k / (usable_pct / 100)
137
+ tax_multiplier = effective_cost_per_1k / nominal_price_per_1k
138
+ ```
139
+
140
+ If `usable_count` is `0`, the effective cost is **Infinite**: no amount of
141
+ nominal budget buys a usable response at that sample rate. The JSON output
142
+ sets `effective_cost_per_1k` and `tax_multiplier` to `null` and
143
+ `cost_status` to `"infinite"`, which keeps the document valid JSON.
144
+
145
+ ### Worked example
146
+
147
+ You pay **$0.49 per 1,000 requests**. You sample 20 requests and 14 come back
148
+ usable with the expected marker.
149
+
150
+ ```text
151
+ usable_pct = 14 / 20 * 100 = 70.0%
152
+ effective_cost_per_1k = 0.49 / 0.70 = $0.70
153
+ tax_multiplier = 0.70 / 0.49 = 1.43x
154
+ ```
155
+
156
+ At 1,000,000 requests per month the invoice is `$490`. Because only 70% of
157
+ attempts are usable, you need about 1,428,571 attempts to obtain 1,000,000
158
+ usable responses, which costs about `$700`. **The 403 Tax is $210 per month**
159
+ on that volume.
160
+
161
+ The model assumes blocked attempts are billed, which is how per-request and
162
+ per-IP plans normally work. If your provider bills only successful requests,
163
+ set `--price-per-1k` to the effective rate you want to test and read the
164
+ multiplier as a quality signal rather than a surcharge.
165
+
166
+ ## Options
167
+
168
+ | Option | Default | Description |
169
+ | --- | --- | --- |
170
+ | `--url URL` | required | Absolute `http(s)` URL to probe. |
171
+ | `--requests N` | `20` | Number of samples. |
172
+ | `--marker STRING` | none | String that must appear in the body for a sample to count as usable. |
173
+ | `--price-per-1k FLOAT` | `0.49` | Nominal price per 1,000 requests, in USD. |
174
+ | `--proxy URL` | none | HTTP(S) proxy, e.g. `http://user:pass@host:port`. |
175
+ | `--concurrency N` | `4` | Maximum in-flight requests. |
176
+ | `--timeout SECONDS` | `10` | Per-request timeout. |
177
+ | `--json` | off | Emit one JSON object and nothing else. |
178
+ | `--version` | | Print the version and exit. |
179
+ | `--help` | | Show help, including the FTC disclosure. |
180
+
181
+ ## Exit codes
182
+
183
+ | Code | Meaning |
184
+ | --- | --- |
185
+ | `0` | Every sampled request was usable. |
186
+ | `1` | Degraded: at least one request was blocked, at least one succeeded. |
187
+ | `2` | Failed: no request was usable. |
188
+ | `3` | Invalid arguments or URL. |
189
+
190
+ This makes the CLI useful as a gate without parsing text: `0` is the only
191
+ fully healthy result. A CI job that must not fail on a degraded sample can
192
+ capture the code and inspect the JSON instead.
193
+
194
+ ## JSON output
195
+
196
+ `--json` writes a single JSON object to stdout, suitable for `jq`, dashboards,
197
+ or a CI artifact. Diagnostics never share that stream.
198
+
199
+ ```json
200
+ {
201
+ "tool": "check-proxy-tax",
202
+ "version": "0.1.0",
203
+ "url": "https://example.com/product/123",
204
+ "marker": "Add to cart",
205
+ "requests": 20,
206
+ "concurrency": 4,
207
+ "timeout_seconds": 10.0,
208
+ "proxy_configured": false,
209
+ "nominal_price_per_1k": 0.49,
210
+ "usable_count": 14,
211
+ "blocked_count": 6,
212
+ "usable_pct": 70.0,
213
+ "effective_cost_per_1k": 0.7,
214
+ "tax_multiplier": 1.428571,
215
+ "cost_status": "finite",
216
+ "latency_ms": {
217
+ "p50": 212.4,
218
+ "p95": 1840.9,
219
+ "min": 141.2,
220
+ "max": 2190.7
221
+ },
222
+ "total_bytes": 1234567,
223
+ "status_counts": {
224
+ "200": 14,
225
+ "403": 5,
226
+ "timeout": 1
227
+ },
228
+ "blocked_reasons": {
229
+ "http_403": 5,
230
+ "timeout": 1
231
+ },
232
+ "exit_code": 1,
233
+ "benchmark_url": "https://scrape.mybskm.top/?utm_source=cli&utm_content=check-proxy-tax",
234
+ "ftc_disclosure": "FTC Disclosure: Benchmark links may include affiliate tracking."
235
+ }
236
+ ```
237
+
238
+ `status_counts` uses the numeric HTTP status as a string, or `timeout` /
239
+ `error` when no status was received. `blocked_reasons` uses `http_<status>`,
240
+ `marker_missing`, `timeout`, or `connection_error`.
241
+
242
+ ## CI example
243
+
244
+ ```yaml
245
+ name: proxy-cost-gate
246
+ on:
247
+ workflow_dispatch:
248
+ schedule:
249
+ - cron: "17 6 * * *"
250
+
251
+ jobs:
252
+ audit:
253
+ runs-on: ubuntu-latest
254
+ steps:
255
+ - uses: actions/checkout@v4
256
+ - uses: actions/setup-python@v5
257
+ with:
258
+ python-version: "3.12"
259
+ - run: python3 -m pip install .
260
+ - name: Audit effective proxy cost
261
+ env:
262
+ TARGET_URL: https://example.com/product/123
263
+ run: |
264
+ set +e
265
+ check-proxy-tax \
266
+ --url "$TARGET_URL" \
267
+ --requests 30 \
268
+ --marker "Add to cart" \
269
+ --price-per-1k 0.49 \
270
+ --json > proxy-report.json
271
+ code=$?
272
+ set -e
273
+ cat proxy-report.json
274
+ # Gate on usable rate, not on the raw exit code.
275
+ jq -e '.usable_pct >= 95 and .cost_status == "finite"' proxy-report.json
276
+ ```
277
+
278
+ Run the suite locally with:
279
+
280
+ ```bash
281
+ python3 -m unittest test_cli.py -v
282
+ ```
283
+
284
+ The tests use a local `http.server`; they do not touch the network.
285
+
286
+ ## Benchmark and provider matrix
287
+
288
+ Public benchmark and provider comparison:
289
+
290
+ <https://scrape.mybskm.top/?utm_source=cli&utm_content=check-proxy-tax>
291
+
292
+ FTC Disclosure: Benchmark links may include affiliate tracking.
293
+
294
+ ## License
295
+
296
+ MIT. See [LICENSE](LICENSE).
297
+
298
+ Copyright (c) 2026 Kais / 600me.
@@ -0,0 +1,267 @@
1
+ # check-proxy-tax
2
+
3
+ **Find out what a "cheap" proxy actually costs per 1,000 usable requests.**
4
+
5
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/)
6
+ [![Zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](#install)
7
+ [![MIT License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
8
+
9
+ Proxy invoices are priced per request, per IP, or per GB. Your scraper is not
10
+ paid in those units. It is paid in responses it can actually use. When an edge
11
+ returns `403`, a request stalls until timeout, or a `200` arrives without the
12
+ product marker, the attempt is still billed. The gap between the advertised
13
+ price and the price of usable traffic is **The 403 Tax**.
14
+
15
+ `check-proxy-tax` measures that gap directly. It fires a small concurrent burst
16
+ at one URL, classifies every response as usable or blocked, and reports:
17
+
18
+ - usable rate and blocked/failed counts;
19
+ - effective cost per 1,000 usable requests;
20
+ - the tax multiplier over the nominal per-1,000 price;
21
+ - p50 / p95 latency and total bytes transferred;
22
+ - status-code and failure-reason breakdown.
23
+
24
+ It is a single core module using only the standard library. No account, no API
25
+ key, no external dependencies, no background service.
26
+
27
+ ## Install
28
+
29
+ Python 3.8 or newer.
30
+
31
+ ```bash
32
+ git clone https://github.com/600me/check-proxy-tax.git
33
+ cd check-proxy-tax
34
+ python3 -m pip install .
35
+ check-proxy-tax --help
36
+ ```
37
+
38
+ You can also run it straight from a checkout without installing:
39
+
40
+ ```bash
41
+ python3 -m check_proxy_tax.cli --url https://example.com --requests 5
42
+ ```
43
+
44
+ The package is PyPI-ready (`python3 -m build` produces a wheel and sdist), but
45
+ installing from source is the supported path until the first PyPI release.
46
+
47
+ ## Quick start
48
+
49
+ ```bash
50
+ check-proxy-tax \
51
+ --url "https://example.com/product/123" \
52
+ --requests 20 \
53
+ --marker "Add to cart" \
54
+ --price-per-1k 0.49
55
+ ```
56
+
57
+ ```text
58
+ check-proxy-tax 0.1.0 - proxy economics audit
59
+
60
+ +---------------------+------------------------------------------+
61
+ | METRIC | VALUE |
62
+ +---------------------+------------------------------------------+
63
+ | Target URL | https://example.com/product/123 |
64
+ | Samples | 20 (concurrency 4, timeout 10s) |
65
+ | Marker | Add to cart |
66
+ | Usable success | 14 / 20 |
67
+ | Blocked / failed | 6 / 20 |
68
+ | Usable rate | 70.0% |
69
+ +---------------------+------------------------------------------+
70
+ | Nominal price / 1k | $0.4900 |
71
+ | Effective cost / 1k | $0.7000 |
72
+ | Tax multiplier | 1.43x |
73
+ +---------------------+------------------------------------------+
74
+ | Latency p50 | 212.4 ms |
75
+ | Latency p95 | 1840.9 ms |
76
+ | Total transferred | 1.18 MB (1,234,567 bytes) |
77
+ | Status codes | 200:14, 403:5, timeout:1 |
78
+ | Blocked reasons | http_403:5, timeout:1 |
79
+ | Verdict | DEGRADED - 6 of 20 requests blocked |
80
+ +---------------------+------------------------------------------+
81
+
82
+ Benchmark & provider matrix: https://scrape.mybskm.top/?utm_source=cli&utm_content=check-proxy-tax
83
+ FTC Disclosure: Benchmark links may include affiliate tracking.
84
+ ```
85
+
86
+ ## What counts as usable
87
+
88
+ A sample is **usable** only when both conditions hold:
89
+
90
+ 1. the final HTTP status is exactly `200`;
91
+ 2. when `--marker` is supplied, the response body contains that string
92
+ (case-sensitive, matched against the raw UTF-8 bytes).
93
+
94
+ Everything else is **blocked**: `403`, `429`, other `4xx`, `5xx`, timeout,
95
+ connection/TLS failure, or a `200` without the marker. Redirects are followed
96
+ and the final status is classified.
97
+
98
+ That definition is deliberately strict. A `200` challenge page, a consent
99
+ screen, or an empty shell is not inventory your scraper can use.
100
+
101
+ ## The math
102
+
103
+ ```text
104
+ usable_pct = usable_count / total_requests * 100
105
+ effective_cost_per_1k = nominal_price_per_1k / (usable_pct / 100)
106
+ tax_multiplier = effective_cost_per_1k / nominal_price_per_1k
107
+ ```
108
+
109
+ If `usable_count` is `0`, the effective cost is **Infinite**: no amount of
110
+ nominal budget buys a usable response at that sample rate. The JSON output
111
+ sets `effective_cost_per_1k` and `tax_multiplier` to `null` and
112
+ `cost_status` to `"infinite"`, which keeps the document valid JSON.
113
+
114
+ ### Worked example
115
+
116
+ You pay **$0.49 per 1,000 requests**. You sample 20 requests and 14 come back
117
+ usable with the expected marker.
118
+
119
+ ```text
120
+ usable_pct = 14 / 20 * 100 = 70.0%
121
+ effective_cost_per_1k = 0.49 / 0.70 = $0.70
122
+ tax_multiplier = 0.70 / 0.49 = 1.43x
123
+ ```
124
+
125
+ At 1,000,000 requests per month the invoice is `$490`. Because only 70% of
126
+ attempts are usable, you need about 1,428,571 attempts to obtain 1,000,000
127
+ usable responses, which costs about `$700`. **The 403 Tax is $210 per month**
128
+ on that volume.
129
+
130
+ The model assumes blocked attempts are billed, which is how per-request and
131
+ per-IP plans normally work. If your provider bills only successful requests,
132
+ set `--price-per-1k` to the effective rate you want to test and read the
133
+ multiplier as a quality signal rather than a surcharge.
134
+
135
+ ## Options
136
+
137
+ | Option | Default | Description |
138
+ | --- | --- | --- |
139
+ | `--url URL` | required | Absolute `http(s)` URL to probe. |
140
+ | `--requests N` | `20` | Number of samples. |
141
+ | `--marker STRING` | none | String that must appear in the body for a sample to count as usable. |
142
+ | `--price-per-1k FLOAT` | `0.49` | Nominal price per 1,000 requests, in USD. |
143
+ | `--proxy URL` | none | HTTP(S) proxy, e.g. `http://user:pass@host:port`. |
144
+ | `--concurrency N` | `4` | Maximum in-flight requests. |
145
+ | `--timeout SECONDS` | `10` | Per-request timeout. |
146
+ | `--json` | off | Emit one JSON object and nothing else. |
147
+ | `--version` | | Print the version and exit. |
148
+ | `--help` | | Show help, including the FTC disclosure. |
149
+
150
+ ## Exit codes
151
+
152
+ | Code | Meaning |
153
+ | --- | --- |
154
+ | `0` | Every sampled request was usable. |
155
+ | `1` | Degraded: at least one request was blocked, at least one succeeded. |
156
+ | `2` | Failed: no request was usable. |
157
+ | `3` | Invalid arguments or URL. |
158
+
159
+ This makes the CLI useful as a gate without parsing text: `0` is the only
160
+ fully healthy result. A CI job that must not fail on a degraded sample can
161
+ capture the code and inspect the JSON instead.
162
+
163
+ ## JSON output
164
+
165
+ `--json` writes a single JSON object to stdout, suitable for `jq`, dashboards,
166
+ or a CI artifact. Diagnostics never share that stream.
167
+
168
+ ```json
169
+ {
170
+ "tool": "check-proxy-tax",
171
+ "version": "0.1.0",
172
+ "url": "https://example.com/product/123",
173
+ "marker": "Add to cart",
174
+ "requests": 20,
175
+ "concurrency": 4,
176
+ "timeout_seconds": 10.0,
177
+ "proxy_configured": false,
178
+ "nominal_price_per_1k": 0.49,
179
+ "usable_count": 14,
180
+ "blocked_count": 6,
181
+ "usable_pct": 70.0,
182
+ "effective_cost_per_1k": 0.7,
183
+ "tax_multiplier": 1.428571,
184
+ "cost_status": "finite",
185
+ "latency_ms": {
186
+ "p50": 212.4,
187
+ "p95": 1840.9,
188
+ "min": 141.2,
189
+ "max": 2190.7
190
+ },
191
+ "total_bytes": 1234567,
192
+ "status_counts": {
193
+ "200": 14,
194
+ "403": 5,
195
+ "timeout": 1
196
+ },
197
+ "blocked_reasons": {
198
+ "http_403": 5,
199
+ "timeout": 1
200
+ },
201
+ "exit_code": 1,
202
+ "benchmark_url": "https://scrape.mybskm.top/?utm_source=cli&utm_content=check-proxy-tax",
203
+ "ftc_disclosure": "FTC Disclosure: Benchmark links may include affiliate tracking."
204
+ }
205
+ ```
206
+
207
+ `status_counts` uses the numeric HTTP status as a string, or `timeout` /
208
+ `error` when no status was received. `blocked_reasons` uses `http_<status>`,
209
+ `marker_missing`, `timeout`, or `connection_error`.
210
+
211
+ ## CI example
212
+
213
+ ```yaml
214
+ name: proxy-cost-gate
215
+ on:
216
+ workflow_dispatch:
217
+ schedule:
218
+ - cron: "17 6 * * *"
219
+
220
+ jobs:
221
+ audit:
222
+ runs-on: ubuntu-latest
223
+ steps:
224
+ - uses: actions/checkout@v4
225
+ - uses: actions/setup-python@v5
226
+ with:
227
+ python-version: "3.12"
228
+ - run: python3 -m pip install .
229
+ - name: Audit effective proxy cost
230
+ env:
231
+ TARGET_URL: https://example.com/product/123
232
+ run: |
233
+ set +e
234
+ check-proxy-tax \
235
+ --url "$TARGET_URL" \
236
+ --requests 30 \
237
+ --marker "Add to cart" \
238
+ --price-per-1k 0.49 \
239
+ --json > proxy-report.json
240
+ code=$?
241
+ set -e
242
+ cat proxy-report.json
243
+ # Gate on usable rate, not on the raw exit code.
244
+ jq -e '.usable_pct >= 95 and .cost_status == "finite"' proxy-report.json
245
+ ```
246
+
247
+ Run the suite locally with:
248
+
249
+ ```bash
250
+ python3 -m unittest test_cli.py -v
251
+ ```
252
+
253
+ The tests use a local `http.server`; they do not touch the network.
254
+
255
+ ## Benchmark and provider matrix
256
+
257
+ Public benchmark and provider comparison:
258
+
259
+ <https://scrape.mybskm.top/?utm_source=cli&utm_content=check-proxy-tax>
260
+
261
+ FTC Disclosure: Benchmark links may include affiliate tracking.
262
+
263
+ ## License
264
+
265
+ MIT. See [LICENSE](LICENSE).
266
+
267
+ Copyright (c) 2026 Kais / 600me.
@@ -0,0 +1,5 @@
1
+ """check-proxy-tax: audit effective proxy cost per 1,000 usable requests."""
2
+
3
+ __version__ = "1.0.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,519 @@
1
+ """check-proxy-tax: measure what usable proxy traffic actually costs.
2
+
3
+ The tool sends a burst of concurrent requests to one URL, classifies every
4
+ response as usable (HTTP 200 plus an optional marker) or blocked, and converts
5
+ the raw per-1,000 price into the price you really pay per 1,000 *usable*
6
+ responses.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import math
14
+ import socket
15
+ import statistics
16
+ import sys
17
+ import time
18
+ import urllib.error
19
+ import urllib.parse
20
+ import urllib.request
21
+ from collections import Counter
22
+ from concurrent.futures import ThreadPoolExecutor
23
+ from dataclasses import dataclass
24
+ from typing import Dict, List, Optional, Sequence, Tuple
25
+
26
+ __version__ = "1.0.0"
27
+
28
+ BENCHMARK_URL = (
29
+ "https://scrape.mybskm.top/?utm_source=cli&utm_content=check-proxy-tax"
30
+ )
31
+ FTC_DISCLOSURE = (
32
+ "FTC Disclosure: Benchmark links may include affiliate tracking."
33
+ )
34
+ USER_AGENT = (
35
+ "check-proxy-tax/{0} (+https://github.com/600me/check-proxy-tax)".format(
36
+ __version__
37
+ )
38
+ )
39
+
40
+ EXIT_USABLE = 0
41
+ EXIT_DEGRADED = 1
42
+ EXIT_FAILED = 2
43
+ EXIT_USAGE = 3
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class Sample:
48
+ """One completed probe attempt."""
49
+
50
+ usable: bool
51
+ status: Optional[int]
52
+ reason: str
53
+ latency_ms: float
54
+ body_bytes: int
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class Metrics:
59
+ """Aggregated economics for one run."""
60
+
61
+ total: int
62
+ usable: int
63
+ usable_pct: float
64
+ effective_cost_per_1k: Optional[float]
65
+ tax_multiplier: Optional[float]
66
+ cost_status: str
67
+
68
+
69
+ def _timeout_reason(exc: BaseException) -> str:
70
+ reason = getattr(exc, "reason", exc)
71
+ text = str(reason).lower()
72
+ if isinstance(exc, (socket.timeout, TimeoutError)) or "timed out" in text:
73
+ return "timeout"
74
+ if "timeout" in text:
75
+ return "timeout"
76
+ return "connection_error"
77
+
78
+
79
+ def fetch_once(
80
+ url: str,
81
+ marker: Optional[str],
82
+ timeout: float,
83
+ proxy: Optional[str] = None,
84
+ ) -> Sample:
85
+ """Fetch *url* once and classify the result.
86
+
87
+ Usable means: final HTTP status is exactly 200 and, when a marker was
88
+ supplied, the raw response body contains that marker as UTF-8 bytes.
89
+ """
90
+
91
+ handlers: List[urllib.request.BaseHandler] = []
92
+ if proxy:
93
+ handlers.append(
94
+ urllib.request.ProxyHandler({"http": proxy, "https": proxy})
95
+ )
96
+ opener = urllib.request.build_opener(*handlers)
97
+ request = urllib.request.Request(
98
+ url,
99
+ headers={"User-Agent": USER_AGENT, "Accept": "*/*"},
100
+ )
101
+
102
+ started = time.perf_counter()
103
+ try:
104
+ with opener.open(request, timeout=timeout) as response:
105
+ body = response.read()
106
+ elapsed_ms = (time.perf_counter() - started) * 1000.0
107
+ status = int(response.status)
108
+ if status != 200:
109
+ return Sample(False, status, "http_{0}".format(status), elapsed_ms, len(body))
110
+ if marker is not None and marker.encode("utf-8") not in body:
111
+ return Sample(False, status, "marker_missing", elapsed_ms, len(body))
112
+ return Sample(True, status, "ok", elapsed_ms, len(body))
113
+ except urllib.error.HTTPError as exc:
114
+ elapsed_ms = (time.perf_counter() - started) * 1000.0
115
+ try:
116
+ body_bytes = len(exc.read())
117
+ except Exception:
118
+ body_bytes = 0
119
+ return Sample(
120
+ False,
121
+ int(exc.code),
122
+ "http_{0}".format(exc.code),
123
+ elapsed_ms,
124
+ body_bytes,
125
+ )
126
+ except Exception as exc: # URLError, timeouts, TLS and socket errors
127
+ elapsed_ms = (time.perf_counter() - started) * 1000.0
128
+ return Sample(False, None, _timeout_reason(exc), elapsed_ms, 0)
129
+
130
+
131
+ def compute_metrics(usable: int, total: int, nominal_price: float) -> Metrics:
132
+ """Apply the effective-cost formulas to a finished sample set."""
133
+
134
+ if total < 1:
135
+ raise ValueError("total must be >= 1")
136
+ if usable < 0 or usable > total:
137
+ raise ValueError("usable must be between 0 and total")
138
+ if nominal_price <= 0:
139
+ raise ValueError("nominal_price must be > 0")
140
+
141
+ usable_pct = (usable / total) * 100.0
142
+ if usable == 0:
143
+ return Metrics(total, usable, usable_pct, None, None, "infinite")
144
+
145
+ effective_cost = nominal_price / (usable_pct / 100.0)
146
+ tax_multiplier = effective_cost / nominal_price
147
+ return Metrics(
148
+ total,
149
+ usable,
150
+ usable_pct,
151
+ effective_cost,
152
+ tax_multiplier,
153
+ "finite",
154
+ )
155
+
156
+
157
+ def percentile(values: Sequence[float], pct: float) -> Optional[float]:
158
+ """Linear-interpolated percentile (the common numpy/Excel definition)."""
159
+
160
+ if not values:
161
+ return None
162
+ ordered = sorted(values)
163
+ if len(ordered) == 1:
164
+ return ordered[0]
165
+ if pct == 50:
166
+ return float(statistics.median(ordered))
167
+ rank = (len(ordered) - 1) * (pct / 100.0)
168
+ lower = int(math.floor(rank))
169
+ upper = int(math.ceil(rank))
170
+ if lower == upper:
171
+ return ordered[lower]
172
+ fraction = rank - lower
173
+ return ordered[lower] + (ordered[upper] - ordered[lower]) * fraction
174
+
175
+
176
+ def _exit_code(metrics: Metrics) -> int:
177
+ if metrics.usable == metrics.total:
178
+ return EXIT_USABLE
179
+ if metrics.usable == 0:
180
+ return EXIT_FAILED
181
+ return EXIT_DEGRADED
182
+
183
+
184
+ def _money(value: Optional[float]) -> str:
185
+ if value is None:
186
+ return "Infinite"
187
+ return "${0:,.4f}".format(value)
188
+
189
+
190
+ def _multiplier(value: Optional[float]) -> str:
191
+ if value is None:
192
+ return "Infinite"
193
+ return "{0:,.2f}x".format(value)
194
+
195
+
196
+ def _human_bytes(count: int) -> str:
197
+ if count < 1024:
198
+ return "{0} B".format(count)
199
+ value = float(count)
200
+ for unit in ("KB", "MB", "GB", "TB"):
201
+ value /= 1024.0
202
+ if value < 1024.0:
203
+ return "{0:.2f} {1} ({2:,} bytes)".format(value, unit, count)
204
+ return "{0:.2f} PB ({1:,} bytes)".format(value, count)
205
+
206
+
207
+ def _table(rows: List[Tuple[str, str]], separators: Sequence[int]) -> str:
208
+ label_width = max([len("METRIC")] + [len(label) for label, _ in rows])
209
+ value_width = max([len("VALUE")] + [len(value) for _, value in rows])
210
+ border = "+{0}+{1}+".format(
211
+ "-" * (label_width + 2), "-" * (value_width + 2)
212
+ )
213
+
214
+ def row(label: str, value: str) -> str:
215
+ return "| {0:<{1}} | {2:<{3}} |".format(
216
+ label, label_width, value, value_width
217
+ )
218
+
219
+ lines = [border, row("METRIC", "VALUE"), border]
220
+ for index, (label, value) in enumerate(rows):
221
+ lines.append(row(label, value))
222
+ if index in separators:
223
+ lines.append(border)
224
+ lines.append(border)
225
+ return "\n".join(lines)
226
+
227
+
228
+ def render_table(report: Dict[str, object]) -> str:
229
+ """Render the human-readable ASCII diagnostic table."""
230
+
231
+ latency = report["latency_ms"] # type: ignore[index]
232
+ status_counts = report["status_counts"] # type: ignore[index]
233
+ blocked_reasons = report["blocked_reasons"] # type: ignore[index]
234
+
235
+ def pairs(mapping: Dict[str, int]) -> str:
236
+ if not mapping:
237
+ return "none"
238
+ return ", ".join(
239
+ "{0}:{1}".format(key, value)
240
+ for key, value in sorted(mapping.items())
241
+ )
242
+
243
+ nominal = float(report["nominal_price_per_1k"]) # type: ignore[arg-type]
244
+ effective = report["effective_cost_per_1k"]
245
+ multiplier = report["tax_multiplier"]
246
+
247
+ rows: List[Tuple[str, str]] = [
248
+ ("Target URL", str(report["url"])),
249
+ (
250
+ "Samples",
251
+ "{0} (concurrency {1}, timeout {2:g}s)".format(
252
+ report["requests"], report["concurrency"], report["timeout_seconds"]
253
+ ),
254
+ ),
255
+ ("Marker", str(report["marker"]) if report["marker"] else "(none)"),
256
+ (
257
+ "Usable success",
258
+ "{0} / {1}".format(report["usable_count"], report["requests"]),
259
+ ),
260
+ (
261
+ "Blocked / failed",
262
+ "{0} / {1}".format(report["blocked_count"], report["requests"]),
263
+ ),
264
+ ("Usable rate", "{0:.1f}%".format(float(report["usable_pct"]))),
265
+ ("Nominal price / 1k", _money(nominal)),
266
+ (
267
+ "Effective cost / 1k",
268
+ _money(float(effective) if effective is not None else None),
269
+ ),
270
+ (
271
+ "Tax multiplier",
272
+ _multiplier(float(multiplier) if multiplier is not None else None),
273
+ ),
274
+ ("Latency p50", "{0:.1f} ms".format(float(latency["p50"]))), # type: ignore[index]
275
+ ("Latency p95", "{0:.1f} ms".format(float(latency["p95"]))), # type: ignore[index]
276
+ ("Total transferred", _human_bytes(int(report["total_bytes"]))),
277
+ ("Status codes", pairs(status_counts)), # type: ignore[arg-type]
278
+ ("Blocked reasons", pairs(blocked_reasons)), # type: ignore[arg-type]
279
+ ("Verdict", _verdict_from_report(report)),
280
+ ]
281
+
282
+ title = "check-proxy-tax {0} - proxy economics audit".format(__version__)
283
+ return "{0}\n\n{1}".format(title, _table(rows, separators=(5, 8)))
284
+
285
+
286
+ def _verdict_from_report(report: Dict[str, object]) -> str:
287
+ usable = int(report["usable_count"]) # type: ignore[arg-type]
288
+ total = int(report["requests"]) # type: ignore[arg-type]
289
+ if usable == total:
290
+ return "PASS - every sampled request was usable"
291
+ if usable == 0:
292
+ return "FAIL - no usable responses"
293
+ return "DEGRADED - {0} of {1} requests blocked".format(total - usable, total)
294
+
295
+
296
+ def build_report(
297
+ args: argparse.Namespace,
298
+ samples: Sequence[Sample],
299
+ metrics: Metrics,
300
+ ) -> Dict[str, object]:
301
+ """Build the JSON-serialisable report shared by both output modes."""
302
+
303
+ latencies = [sample.latency_ms for sample in samples]
304
+ status_counts: Counter = Counter(
305
+ "timeout"
306
+ if sample.reason == "timeout"
307
+ else "error"
308
+ if sample.status is None
309
+ else str(sample.status)
310
+ for sample in samples
311
+ )
312
+ blocked_reasons: Counter = Counter(
313
+ sample.reason for sample in samples if not sample.usable
314
+ )
315
+
316
+ p50 = percentile(latencies, 50)
317
+ p95 = percentile(latencies, 95)
318
+ code = _exit_code(metrics)
319
+
320
+ return {
321
+ "tool": "check-proxy-tax",
322
+ "version": __version__,
323
+ "url": args.url,
324
+ "marker": args.marker,
325
+ "requests": args.requests,
326
+ "concurrency": args.concurrency,
327
+ "timeout_seconds": args.timeout,
328
+ "proxy_configured": bool(args.proxy),
329
+ "nominal_price_per_1k": args.price_per_1k,
330
+ "usable_count": metrics.usable,
331
+ "blocked_count": metrics.total - metrics.usable,
332
+ "usable_pct": round(metrics.usable_pct, 4),
333
+ "effective_cost_per_1k": (
334
+ round(metrics.effective_cost_per_1k, 6)
335
+ if metrics.effective_cost_per_1k is not None
336
+ else None
337
+ ),
338
+ "tax_multiplier": (
339
+ round(metrics.tax_multiplier, 6)
340
+ if metrics.tax_multiplier is not None
341
+ else None
342
+ ),
343
+ "cost_status": metrics.cost_status,
344
+ "latency_ms": {
345
+ "p50": round(p50, 2) if p50 is not None else None,
346
+ "p95": round(p95, 2) if p95 is not None else None,
347
+ "min": round(min(latencies), 2),
348
+ "max": round(max(latencies), 2),
349
+ },
350
+ "total_bytes": sum(sample.body_bytes for sample in samples),
351
+ "status_counts": dict(sorted(status_counts.items())),
352
+ "blocked_reasons": dict(sorted(blocked_reasons.items())),
353
+ "exit_code": code,
354
+ "benchmark_url": BENCHMARK_URL,
355
+ "ftc_disclosure": FTC_DISCLOSURE,
356
+ }
357
+
358
+
359
+ def _positive_int(value: str) -> int:
360
+ try:
361
+ number = int(value)
362
+ except ValueError:
363
+ raise argparse.ArgumentTypeError("must be an integer")
364
+ if number < 1:
365
+ raise argparse.ArgumentTypeError("must be >= 1")
366
+ return number
367
+
368
+
369
+ def _positive_float(value: str) -> float:
370
+ try:
371
+ number = float(value)
372
+ except ValueError:
373
+ raise argparse.ArgumentTypeError("must be a number")
374
+ if not math.isfinite(number) or number <= 0:
375
+ raise argparse.ArgumentTypeError("must be a positive number")
376
+ return number
377
+
378
+
379
+ class _ArgumentParser(argparse.ArgumentParser):
380
+ """Keep argument errors distinct from a run where every probe failed."""
381
+
382
+ def error(self, message: str) -> None: # type: ignore[override]
383
+ self.print_usage(sys.stderr)
384
+ self.exit(EXIT_USAGE, "{0}: error: {1}\n".format(self.prog, message))
385
+
386
+
387
+ def build_parser() -> argparse.ArgumentParser:
388
+ parser = _ArgumentParser(
389
+ prog="check-proxy-tax",
390
+ description=(
391
+ "Sample a URL concurrently and report the effective cost per "
392
+ "1,000 usable responses after 403s, 429s, 5xx errors, timeouts, "
393
+ "and marker misses."
394
+ ),
395
+ formatter_class=argparse.RawDescriptionHelpFormatter,
396
+ epilog=(
397
+ "examples:\n"
398
+ " check-proxy-tax --url https://example.com/product --requests 20 \\\n"
399
+ " --marker \"Add to cart\" --price-per-1k 0.49\n"
400
+ " check-proxy-tax --url https://example.com --json > report.json\n"
401
+ "\n"
402
+ "exit codes:\n"
403
+ " 0 every sampled request was usable\n"
404
+ " 1 some requests were blocked (degraded)\n"
405
+ " 2 no request was usable\n"
406
+ " 3 invalid arguments\n"
407
+ "\n"
408
+ "Benchmark & provider matrix: {0}\n"
409
+ "\n"
410
+ "{1}".format(BENCHMARK_URL, FTC_DISCLOSURE)
411
+ ),
412
+ )
413
+ parser.add_argument("--url", required=True, help="absolute http(s) URL to probe")
414
+ parser.add_argument(
415
+ "--requests",
416
+ type=_positive_int,
417
+ default=20,
418
+ help="number of concurrent samples (default: 20)",
419
+ )
420
+ parser.add_argument(
421
+ "--marker",
422
+ default=None,
423
+ help="string that must appear in the response body for a usable sample",
424
+ )
425
+ parser.add_argument(
426
+ "--price-per-1k",
427
+ type=_positive_float,
428
+ default=0.49,
429
+ help="nominal price per 1,000 requests in USD (default: 0.49)",
430
+ )
431
+ parser.add_argument(
432
+ "--proxy",
433
+ default=None,
434
+ help="HTTP(S) proxy URL, e.g. http://user:pass@host:port",
435
+ )
436
+ parser.add_argument(
437
+ "--concurrency",
438
+ type=_positive_int,
439
+ default=4,
440
+ help="maximum in-flight requests (default: 4)",
441
+ )
442
+ parser.add_argument(
443
+ "--timeout",
444
+ type=_positive_float,
445
+ default=10.0,
446
+ help="per-request timeout in seconds (default: 10)",
447
+ )
448
+ parser.add_argument(
449
+ "--json",
450
+ action="store_true",
451
+ dest="as_json",
452
+ help="emit one JSON object for CI/CD pipelines",
453
+ )
454
+ parser.add_argument(
455
+ "--version",
456
+ action="version",
457
+ version="check-proxy-tax {0}".format(__version__),
458
+ )
459
+ return parser
460
+
461
+
462
+ def run_probe(
463
+ url: str,
464
+ requests: int,
465
+ marker: Optional[str],
466
+ timeout: float,
467
+ proxy: Optional[str],
468
+ concurrency: int,
469
+ ) -> List[Sample]:
470
+ workers = max(1, min(concurrency, requests))
471
+ with ThreadPoolExecutor(max_workers=workers) as pool:
472
+ futures = [
473
+ pool.submit(fetch_once, url, marker, timeout, proxy)
474
+ for _ in range(requests)
475
+ ]
476
+ return [future.result() for future in futures]
477
+
478
+
479
+ def main(argv: Optional[Sequence[str]] = None) -> int:
480
+ parser = build_parser()
481
+ args = parser.parse_args(argv)
482
+
483
+ parsed_url = urllib.parse.urlsplit(args.url)
484
+ if parsed_url.scheme not in ("http", "https") or not parsed_url.netloc:
485
+ parser.error("--url must be an absolute http(s) URL")
486
+
487
+ try:
488
+ samples = run_probe(
489
+ args.url,
490
+ args.requests,
491
+ args.marker,
492
+ args.timeout,
493
+ args.proxy,
494
+ args.concurrency,
495
+ )
496
+ except KeyboardInterrupt:
497
+ print("check-proxy-tax: interrupted", file=sys.stderr)
498
+ return 130
499
+
500
+ metrics = compute_metrics(
501
+ sum(1 for sample in samples if sample.usable),
502
+ len(samples),
503
+ args.price_per_1k,
504
+ )
505
+ report = build_report(args, samples, metrics)
506
+
507
+ if args.as_json:
508
+ print(json.dumps(report, indent=2, ensure_ascii=False, sort_keys=False))
509
+ else:
510
+ print(render_table(report))
511
+ print()
512
+ print("Benchmark & provider matrix: {0}".format(BENCHMARK_URL))
513
+ print(FTC_DISCLOSURE)
514
+
515
+ return _exit_code(metrics)
516
+
517
+
518
+ if __name__ == "__main__":
519
+ sys.exit(main())
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "check-proxy-tax"
7
+ version = "1.0.0"
8
+ description = "CLI tool to audit real proxy costs, 403 blowout multipliers, and effective price per 1,000 usable requests."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Kais / 600me" }]
13
+ keywords = [
14
+ "proxy",
15
+ "scraping",
16
+ "benchmark",
17
+ "http",
18
+ "cost",
19
+ "cli",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Environment :: Console",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3 :: Only",
29
+ "Programming Language :: Python :: 3.8",
30
+ "Programming Language :: Python :: 3.9",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Topic :: Internet :: WWW/HTTP",
35
+ "Topic :: System :: Networking",
36
+ "Topic :: Utilities",
37
+ ]
38
+ dependencies = []
39
+
40
+ [project.optional-dependencies]
41
+ dev = ["build>=1.0", "twine>=4.0"]
42
+
43
+ [project.scripts]
44
+ check-proxy-tax = "check_proxy_tax.cli:main"
45
+
46
+ [project.urls]
47
+ Homepage = "https://github.com/600me/check-proxy-tax"
48
+ Repository = "https://github.com/600me/check-proxy-tax"
49
+ Issues = "https://github.com/600me/check-proxy-tax/issues"
50
+ Benchmark = "https://scrape.mybskm.top/?utm_source=cli&utm_content=check-proxy-tax"
51
+
52
+ [tool.setuptools]
53
+ packages = ["check_proxy_tax"]
@@ -0,0 +1,3 @@
1
+ [metadata]
2
+ name = check-proxy-tax
3
+ version = 1.0.0