sentinelsup 0.2.1__tar.gz → 0.2.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sentinelsup
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Sentinel — real-time fraud, VPN, proxy, and bot detection API. Free tier, sub-40ms response.
5
5
  Project-URL: Homepage, https://sntlhq.com
6
6
  Project-URL: Documentation, https://sntlhq.com/api
@@ -92,10 +92,6 @@ print(info["verdict"]) # 'allow' | 'review' | 'block'
92
92
  print(info["signals"]) # {'vpn': ..., 'proxied': ..., 'tor': ..., 'dch': ..., 'anon': ...}
93
93
  ```
94
94
 
95
- > `email=` and `lookup()` ship in the **next PyPI release (0.2.1)** — on PyPI
96
- > today, use the raw HTTP endpoints documented at
97
- > [sntlhq.com/api](https://sntlhq.com/api) until it lands.
98
-
99
95
  ## What you get back
100
96
 
101
97
  `evaluate()` returns an `EvaluateResult` dataclass:
@@ -110,6 +106,10 @@ class EvaluateResult:
110
106
  network: dict # {vpn, proxy, datacenter, anonymous, tor, residential, service}
111
107
  device: dict # antidetect / automation / emulator signals
112
108
  reasons: list[str] # machine-readable codes
109
+ email: dict | None # {disposable: bool} — present when you passed email=
110
+ decision_source: str | None # 'rules' | 'exception' when your policy matched
111
+ engine_decision: str | None # engine's own verdict when policy changed the decision
112
+ test: bool # True for test-token / test-key calls
113
113
  raw: dict # full upstream response
114
114
 
115
115
  is_suspicious: bool # True if decision != 'allow'
@@ -226,6 +226,24 @@ Returns `EvaluateResult`. Raises `SentinelError` on network/API failure.
226
226
 
227
227
  Returns the raw response dict for any public IPv4/IPv6 address (wraps `GET /v1/lookup/{ip}`): `verdict` (`allow`/`review`/`block`), `risk_score` (0–100), `known`, `signals` (`{vpn, proxied, tor, dch, anon}` or `None`), `network` (`{asn, org, country, city}`), `latency_ms`. Shares the per-key hourly quota with `evaluate()`. `known: False` means our feeds hold no data — it is **not** a clean guarantee.
228
228
 
229
+ ## Testing
230
+
231
+ Deterministic test tokens exercise your allow / review / block handling end-to-end — no browser needed. Test calls are authenticated and rate-limited like real ones but never billed, stored, or webhooked, and the response carries `test=True`:
232
+
233
+ ```python
234
+ result = s.evaluate(token="test_vpn") # also: test_clean, test_proxy, test_datacenter, test_tor
235
+ assert result.decision == "review"
236
+ assert result.test
237
+ ```
238
+
239
+ No account yet? The public sandbox key accepts the same test tokens:
240
+
241
+ ```python
242
+ s = Sentinel(api_key="sk_test_sandbox") # CI/staging — nothing billed, nothing stored
243
+ ```
244
+
245
+ Every account also has a personal `sk_test_...` key (Settings → API Key) that runs the *complete* live pipeline — real tokens, your rules and exception pins included — while events stay flagged as test and never count toward usage or webhooks.
246
+
229
247
  ## Errors
230
248
 
231
249
  All failures raise `SentinelError`. The exception carries `.status` (HTTP code) and `.body` (parsed error body) when available.
@@ -1,239 +1,257 @@
1
- # sentinelsup — Sentinel Python SDK
2
-
3
- Official Python SDK for [Sentinel](https://sntlhq.com) — a real-time fraud detection API that flags VPNs, residential proxies, antidetect browsers (Kameleo, GoLogin, Multilogin), Tor exit nodes, and AI bots in under 40 ms.
4
-
5
- [![PyPI](https://img.shields.io/pypi/v/sentinelsup.svg)](https://pypi.org/project/sentinelsup/)
6
- [![Python versions](https://img.shields.io/pypi/pyversions/sentinelsup.svg)](https://pypi.org/project/sentinelsup/)
7
- [![license](https://img.shields.io/pypi/l/sentinelsup.svg)](./LICENSE)
8
-
9
- Zero dependencies — just the standard library. Works with Flask, Django, FastAPI, or bare `urllib`.
10
-
11
- ## Set up with AI (fastest)
12
-
13
- Using Claude Code, Cursor, Copilot, or any AI coding assistant? Paste this one
14
- prompt and it wires the whole integration — frontend script, backend check,
15
- env var, and a test:
16
-
17
- > Fetch https://sntlhq.com/integrate.md and follow it to add Sentinel fraud
18
- > protection to this app — protect signup, login, and checkout. My API key
19
- > is sk_live_YOUR_KEY; put it in a SENTINEL_KEY env var, never in
20
- > client-side code. Then show me how to test it.
21
-
22
- [`integrate.md`](https://sntlhq.com/integrate.md) is the canonical
23
- machine-readable integration guide, kept in sync with the live API.
24
-
25
- ## Install
26
-
27
- ```bash
28
- pip install sentinelsup
29
- ```
30
-
31
- Python 3.8+. Get a free API key (no credit card) at [sntlhq.com/signup](https://sntlhq.com/signup).
32
-
33
- ## Quick start
34
-
35
- ```python
36
- import os
37
- from sentinel import Sentinel
38
-
39
- s = Sentinel(api_key=os.environ["SENTINEL_KEY"]) # or omit — reads the env var itself
40
-
41
- result = s.evaluate(token=request.json["sentinelToken"]) # token from the frontend SDK
42
-
43
- if result.is_blocked: # decision == 'block'
44
- abort(403)
45
-
46
- print(result.decision) # 'allow' | 'review' | 'block' — route on this
47
- print(result.risk_score) # 0..100
48
- print(result.network) # {'vpn': True, 'proxy': False, 'datacenter': True, ...}
49
- print(result.reasons) # ['vpn_detected', 'datacenter_asn', ...]
50
- ```
51
-
52
- Check the signup email against the disposable-domain feed (checked
53
- transiently, never stored), or look up an arbitrary IP with no browser
54
- token at all:
55
-
56
- ```python
57
- result = s.evaluate(token=tok, email=data["email"])
58
- if result.raw.get("email", {}).get("disposable"):
59
- ... # burner domain — decision is escalated allow → review
60
-
61
- info = s.lookup("185.220.101.34") # GET /v1/lookup/{ip} — same key & quota
62
- print(info["verdict"]) # 'allow' | 'review' | 'block'
63
- print(info["signals"]) # {'vpn': ..., 'proxied': ..., 'tor': ..., 'dch': ..., 'anon': ...}
64
- ```
65
-
66
- > `email=` and `lookup()` ship in the **next PyPI release (0.2.1)** — on PyPI
67
- > today, use the raw HTTP endpoints documented at
68
- > [sntlhq.com/api](https://sntlhq.com/api) until it lands.
69
-
70
- ## What you get back
71
-
72
- `evaluate()` returns an `EvaluateResult` dataclass:
73
-
74
- ```python
75
- @dataclass
76
- class EvaluateResult:
77
- decision: str | None # 'allow' | 'review' | 'block'
78
- risk_score: int | None # 0..100
79
- ip: str | None
80
- country: str | None # ISO-2
81
- network: dict # {vpn, proxy, datacenter, anonymous, tor, residential, service}
82
- device: dict # antidetect / automation / emulator signals
83
- reasons: list[str] # machine-readable codes
84
- raw: dict # full upstream response
85
-
86
- is_suspicious: bool # True if decision != 'allow'
87
- is_blocked: bool # True if decision == 'block'
88
- ```
89
-
90
- Try the live sample (same shape, no key needed):
91
-
92
- ```bash
93
- curl "https://sntlhq.com/v1/evaluate/sample?scenario=vpn"
94
- ```
95
-
96
- Or use the [interactive playground](https://sntlhq.com/api#playground).
97
-
98
- ## Frontend setup
99
-
100
- Add the Sentinel SDK to your frontend. One script loads **both** layers —
101
- network (VPN/proxy/datacenter) and device (antidetect/bot/tampering):
102
-
103
- ```html
104
- <script async src="https://sntlhq.com/assets/sentinel.js"></script>
105
-
106
- <!-- Add class="monocle-enriched" to any form you want evaluated -->
107
- <form class="monocle-enriched" id="signup-form">
108
- <!-- The SDK injects both:
109
- <input type="hidden" name="monocle" value="eyJ..."> (network)
110
- <input type="hidden" name="sentinel_fp" value="a1b2..."> (device) -->
111
- </form>
112
- ```
113
-
114
- Forward both fields to your backend with the form submission and pass them to
115
- `evaluate()` as `token` and `fingerprint_event_id` — without the second one,
116
- the device-layer signals (antidetect, automation, emulator) never fire. For
117
- fetch/XHR submissions, collect them explicitly:
118
-
119
- ```js
120
- const { token, fingerprintEventId } = await window.Sentinel.collect();
121
- ```
122
-
123
- ## Examples
124
-
125
- ### Flask — block VPN/proxy signups
126
-
127
- ```python
128
- from flask import Flask, request, abort, jsonify
129
- from sentinel import Sentinel, SentinelError
130
-
131
- app = Flask(__name__)
132
- sentinel = Sentinel() # reads SENTINEL_KEY (or SENTINEL_API_KEY) from env
133
-
134
- @app.route("/signup", methods=["POST"])
135
- def signup():
136
- data = request.get_json()
137
- try:
138
- result = sentinel.evaluate(token=data["sentinelToken"])
139
- except SentinelError as e:
140
- # Fail open OR fail closed — your call. Logged either way.
141
- app.logger.warning("Sentinel error: %s", e)
142
- result = None
143
-
144
- if result and result.is_blocked:
145
- abort(403, "Signup blocked")
146
-
147
- # ... your normal signup flow
148
- return jsonify({"ok": True})
149
- ```
150
-
151
- ### Django — middleware for high-value endpoints
152
-
153
- ```python
154
- from django.http import JsonResponse
155
- from sentinel import Sentinel
156
-
157
- sentinel = Sentinel() # reads SENTINEL_KEY (or SENTINEL_API_KEY) from env
158
-
159
- class FraudCheckMiddleware:
160
- def __init__(self, get_response):
161
- self.get_response = get_response
162
-
163
- def __call__(self, request):
164
- if request.path.startswith("/api/checkout"):
165
- token = request.META.get("HTTP_X_SENTINEL_TOKEN")
166
- if token:
167
- try:
168
- result = sentinel.evaluate(token=token)
169
- if result.is_blocked:
170
- return JsonResponse({"error": "blocked"}, status=403)
171
- except Exception:
172
- pass # fail open
173
- return self.get_response(request)
174
- ```
175
-
176
- Runnable versions live in [`examples/`](./examples/).
177
-
178
- ## API
179
-
180
- ### `Sentinel(api_key=None, endpoint="https://sntlhq.com", timeout=5.0)`
181
-
182
- | Option | Default | Description |
183
- |--------|---------|-------------|
184
- | `api_key` | `$SENTINEL_KEY` (falls back to `$SENTINEL_API_KEY`) | Your key starting with `sk_live_` |
185
- | `endpoint` | `https://sntlhq.com` | Override base URL (for testing) |
186
- | `timeout` | `5.0` | Per-request timeout in seconds |
187
-
188
- ### `sentinel.evaluate(token, fingerprint_event_id=None, account_id=None, email=None)`
189
-
190
- Returns `EvaluateResult`. Raises `SentinelError` on network/API failure.
191
-
192
- - `fingerprint_event_id` — adds the `device` signal block (antidetect, automation, emulator, …).
193
- - `account_id` — your own user id for this session; enables multi-accounting detection (`device.linked_accounts` / `device.multi_account`).
194
- - `email` — adds `email.disposable` to the raw response; burner domains escalate `allow` to `review`.
195
-
196
- ### `sentinel.lookup(ip)`
197
-
198
- Returns the raw response dict for any public IPv4/IPv6 address (wraps `GET /v1/lookup/{ip}`): `verdict` (`allow`/`review`/`block`), `risk_score` (0–100), `known`, `signals` (`{vpn, proxied, tor, dch, anon}` or `None`), `network` (`{asn, org, country, city}`), `latency_ms`. Shares the per-key hourly quota with `evaluate()`. `known: False` means our feeds hold no data — it is **not** a clean guarantee.
199
-
200
- ## Errors
201
-
202
- All failures raise `SentinelError`. The exception carries `.status` (HTTP code) and `.body` (parsed error body) when available.
203
-
204
- ```python
205
- from sentinel import Sentinel, SentinelError
206
-
207
- try:
208
- result = sentinel.evaluate(token=tok)
209
- except SentinelError as e:
210
- if e.status == 429:
211
- pass # back off
212
- elif e.status and 400 <= e.status < 500:
213
- pass # bad input, won't recover by retrying
214
- else:
215
- pass # transient — retry once or fail open
216
- ```
217
-
218
- ## Rate limits
219
-
220
- Free tier: **1,000 requests/hour** per API key. No monthly cap, no credit
221
- card. Upgrade at [sntlhq.com](https://sntlhq.com) when you need more.
222
-
223
- ## What Sentinel detects
224
-
225
- VPNs (commercial + self-hosted) · residential proxies (Bright Data, IPRoyal,
226
- and similar networks) · datacenter IPs · Tor exit nodes · antidetect browsers
227
- (Kameleo, GoLogin, Multilogin, Dolphin{anty}, AdsPower) · headless browsers
228
- and automation (Puppeteer, Playwright, Selenium) · AI agents · emulators and
229
- virtual machines · browser tampering.
230
-
231
- ## Related
232
-
233
- - **Node.js SDK** — [`@sentinelsup/sdk`](https://github.com/sentinelsup/sentinel-node) on npm
234
- - **API docs** — [sntlhq.com/api](https://sntlhq.com/api)
235
- - **Free IP lookup tool** — [sntlhq.com/ip-lookup](https://sntlhq.com/ip-lookup)
236
-
237
- ## License
238
-
239
- MIT © [Sentinel Edge Networks LTD](https://sntlhq.com). See [LICENSE](LICENSE).
1
+ # sentinelsup — Sentinel Python SDK
2
+
3
+ Official Python SDK for [Sentinel](https://sntlhq.com) — a real-time fraud detection API that flags VPNs, residential proxies, antidetect browsers (Kameleo, GoLogin, Multilogin), Tor exit nodes, and AI bots in under 40 ms.
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/sentinelsup.svg)](https://pypi.org/project/sentinelsup/)
6
+ [![Python versions](https://img.shields.io/pypi/pyversions/sentinelsup.svg)](https://pypi.org/project/sentinelsup/)
7
+ [![license](https://img.shields.io/pypi/l/sentinelsup.svg)](./LICENSE)
8
+
9
+ Zero dependencies — just the standard library. Works with Flask, Django, FastAPI, or bare `urllib`.
10
+
11
+ ## Set up with AI (fastest)
12
+
13
+ Using Claude Code, Cursor, Copilot, or any AI coding assistant? Paste this one
14
+ prompt and it wires the whole integration — frontend script, backend check,
15
+ env var, and a test:
16
+
17
+ > Fetch https://sntlhq.com/integrate.md and follow it to add Sentinel fraud
18
+ > protection to this app — protect signup, login, and checkout. My API key
19
+ > is sk_live_YOUR_KEY; put it in a SENTINEL_KEY env var, never in
20
+ > client-side code. Then show me how to test it.
21
+
22
+ [`integrate.md`](https://sntlhq.com/integrate.md) is the canonical
23
+ machine-readable integration guide, kept in sync with the live API.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install sentinelsup
29
+ ```
30
+
31
+ Python 3.8+. Get a free API key (no credit card) at [sntlhq.com/signup](https://sntlhq.com/signup).
32
+
33
+ ## Quick start
34
+
35
+ ```python
36
+ import os
37
+ from sentinel import Sentinel
38
+
39
+ s = Sentinel(api_key=os.environ["SENTINEL_KEY"]) # or omit — reads the env var itself
40
+
41
+ result = s.evaluate(token=request.json["sentinelToken"]) # token from the frontend SDK
42
+
43
+ if result.is_blocked: # decision == 'block'
44
+ abort(403)
45
+
46
+ print(result.decision) # 'allow' | 'review' | 'block' — route on this
47
+ print(result.risk_score) # 0..100
48
+ print(result.network) # {'vpn': True, 'proxy': False, 'datacenter': True, ...}
49
+ print(result.reasons) # ['vpn_detected', 'datacenter_asn', ...]
50
+ ```
51
+
52
+ Check the signup email against the disposable-domain feed (checked
53
+ transiently, never stored), or look up an arbitrary IP with no browser
54
+ token at all:
55
+
56
+ ```python
57
+ result = s.evaluate(token=tok, email=data["email"])
58
+ if result.raw.get("email", {}).get("disposable"):
59
+ ... # burner domain — decision is escalated allow → review
60
+
61
+ info = s.lookup("185.220.101.34") # GET /v1/lookup/{ip} — same key & quota
62
+ print(info["verdict"]) # 'allow' | 'review' | 'block'
63
+ print(info["signals"]) # {'vpn': ..., 'proxied': ..., 'tor': ..., 'dch': ..., 'anon': ...}
64
+ ```
65
+
66
+ ## What you get back
67
+
68
+ `evaluate()` returns an `EvaluateResult` dataclass:
69
+
70
+ ```python
71
+ @dataclass
72
+ class EvaluateResult:
73
+ decision: str | None # 'allow' | 'review' | 'block'
74
+ risk_score: int | None # 0..100
75
+ ip: str | None
76
+ country: str | None # ISO-2
77
+ network: dict # {vpn, proxy, datacenter, anonymous, tor, residential, service}
78
+ device: dict # antidetect / automation / emulator signals
79
+ reasons: list[str] # machine-readable codes
80
+ email: dict | None # {disposable: bool} — present when you passed email=
81
+ decision_source: str | None # 'rules' | 'exception' when your policy matched
82
+ engine_decision: str | None # engine's own verdict when policy changed the decision
83
+ test: bool # True for test-token / test-key calls
84
+ raw: dict # full upstream response
85
+
86
+ is_suspicious: bool # True if decision != 'allow'
87
+ is_blocked: bool # True if decision == 'block'
88
+ ```
89
+
90
+ Try the live sample (same shape, no key needed):
91
+
92
+ ```bash
93
+ curl "https://sntlhq.com/v1/evaluate/sample?scenario=vpn"
94
+ ```
95
+
96
+ Or use the [interactive playground](https://sntlhq.com/api#playground).
97
+
98
+ ## Frontend setup
99
+
100
+ Add the Sentinel SDK to your frontend. One script loads **both** layers —
101
+ network (VPN/proxy/datacenter) and device (antidetect/bot/tampering):
102
+
103
+ ```html
104
+ <script async src="https://sntlhq.com/assets/sentinel.js"></script>
105
+
106
+ <!-- Add class="monocle-enriched" to any form you want evaluated -->
107
+ <form class="monocle-enriched" id="signup-form">
108
+ <!-- The SDK injects both:
109
+ <input type="hidden" name="monocle" value="eyJ..."> (network)
110
+ <input type="hidden" name="sentinel_fp" value="a1b2..."> (device) -->
111
+ </form>
112
+ ```
113
+
114
+ Forward both fields to your backend with the form submission and pass them to
115
+ `evaluate()` as `token` and `fingerprint_event_id` — without the second one,
116
+ the device-layer signals (antidetect, automation, emulator) never fire. For
117
+ fetch/XHR submissions, collect them explicitly:
118
+
119
+ ```js
120
+ const { token, fingerprintEventId } = await window.Sentinel.collect();
121
+ ```
122
+
123
+ ## Examples
124
+
125
+ ### Flask — block VPN/proxy signups
126
+
127
+ ```python
128
+ from flask import Flask, request, abort, jsonify
129
+ from sentinel import Sentinel, SentinelError
130
+
131
+ app = Flask(__name__)
132
+ sentinel = Sentinel() # reads SENTINEL_KEY (or SENTINEL_API_KEY) from env
133
+
134
+ @app.route("/signup", methods=["POST"])
135
+ def signup():
136
+ data = request.get_json()
137
+ try:
138
+ result = sentinel.evaluate(token=data["sentinelToken"])
139
+ except SentinelError as e:
140
+ # Fail open OR fail closed — your call. Logged either way.
141
+ app.logger.warning("Sentinel error: %s", e)
142
+ result = None
143
+
144
+ if result and result.is_blocked:
145
+ abort(403, "Signup blocked")
146
+
147
+ # ... your normal signup flow
148
+ return jsonify({"ok": True})
149
+ ```
150
+
151
+ ### Django — middleware for high-value endpoints
152
+
153
+ ```python
154
+ from django.http import JsonResponse
155
+ from sentinel import Sentinel
156
+
157
+ sentinel = Sentinel() # reads SENTINEL_KEY (or SENTINEL_API_KEY) from env
158
+
159
+ class FraudCheckMiddleware:
160
+ def __init__(self, get_response):
161
+ self.get_response = get_response
162
+
163
+ def __call__(self, request):
164
+ if request.path.startswith("/api/checkout"):
165
+ token = request.META.get("HTTP_X_SENTINEL_TOKEN")
166
+ if token:
167
+ try:
168
+ result = sentinel.evaluate(token=token)
169
+ if result.is_blocked:
170
+ return JsonResponse({"error": "blocked"}, status=403)
171
+ except Exception:
172
+ pass # fail open
173
+ return self.get_response(request)
174
+ ```
175
+
176
+ Runnable versions live in [`examples/`](./examples/).
177
+
178
+ ## API
179
+
180
+ ### `Sentinel(api_key=None, endpoint="https://sntlhq.com", timeout=5.0)`
181
+
182
+ | Option | Default | Description |
183
+ |--------|---------|-------------|
184
+ | `api_key` | `$SENTINEL_KEY` (falls back to `$SENTINEL_API_KEY`) | Your key starting with `sk_live_` |
185
+ | `endpoint` | `https://sntlhq.com` | Override base URL (for testing) |
186
+ | `timeout` | `5.0` | Per-request timeout in seconds |
187
+
188
+ ### `sentinel.evaluate(token, fingerprint_event_id=None, account_id=None, email=None)`
189
+
190
+ Returns `EvaluateResult`. Raises `SentinelError` on network/API failure.
191
+
192
+ - `fingerprint_event_id` — adds the `device` signal block (antidetect, automation, emulator, …).
193
+ - `account_id` — your own user id for this session; enables multi-accounting detection (`device.linked_accounts` / `device.multi_account`).
194
+ - `email` — adds `email.disposable` to the raw response; burner domains escalate `allow` to `review`.
195
+
196
+ ### `sentinel.lookup(ip)`
197
+
198
+ Returns the raw response dict for any public IPv4/IPv6 address (wraps `GET /v1/lookup/{ip}`): `verdict` (`allow`/`review`/`block`), `risk_score` (0–100), `known`, `signals` (`{vpn, proxied, tor, dch, anon}` or `None`), `network` (`{asn, org, country, city}`), `latency_ms`. Shares the per-key hourly quota with `evaluate()`. `known: False` means our feeds hold no data — it is **not** a clean guarantee.
199
+
200
+ ## Testing
201
+
202
+ Deterministic test tokens exercise your allow / review / block handling end-to-end — no browser needed. Test calls are authenticated and rate-limited like real ones but never billed, stored, or webhooked, and the response carries `test=True`:
203
+
204
+ ```python
205
+ result = s.evaluate(token="test_vpn") # also: test_clean, test_proxy, test_datacenter, test_tor
206
+ assert result.decision == "review"
207
+ assert result.test
208
+ ```
209
+
210
+ No account yet? The public sandbox key accepts the same test tokens:
211
+
212
+ ```python
213
+ s = Sentinel(api_key="sk_test_sandbox") # CI/staging — nothing billed, nothing stored
214
+ ```
215
+
216
+ Every account also has a personal `sk_test_...` key (Settings → API Key) that runs the *complete* live pipeline — real tokens, your rules and exception pins included — while events stay flagged as test and never count toward usage or webhooks.
217
+
218
+ ## Errors
219
+
220
+ All failures raise `SentinelError`. The exception carries `.status` (HTTP code) and `.body` (parsed error body) when available.
221
+
222
+ ```python
223
+ from sentinel import Sentinel, SentinelError
224
+
225
+ try:
226
+ result = sentinel.evaluate(token=tok)
227
+ except SentinelError as e:
228
+ if e.status == 429:
229
+ pass # back off
230
+ elif e.status and 400 <= e.status < 500:
231
+ pass # bad input, won't recover by retrying
232
+ else:
233
+ pass # transient — retry once or fail open
234
+ ```
235
+
236
+ ## Rate limits
237
+
238
+ Free tier: **1,000 requests/hour** per API key. No monthly cap, no credit
239
+ card. Upgrade at [sntlhq.com](https://sntlhq.com) when you need more.
240
+
241
+ ## What Sentinel detects
242
+
243
+ VPNs (commercial + self-hosted) · residential proxies (Bright Data, IPRoyal,
244
+ and similar networks) · datacenter IPs · Tor exit nodes · antidetect browsers
245
+ (Kameleo, GoLogin, Multilogin, Dolphin{anty}, AdsPower) · headless browsers
246
+ and automation (Puppeteer, Playwright, Selenium) · AI agents · emulators and
247
+ virtual machines · browser tampering.
248
+
249
+ ## Related
250
+
251
+ - **Node.js SDK** — [`@sentinelsup/sdk`](https://github.com/sentinelsup/sentinel-node) on npm
252
+ - **API docs** — [sntlhq.com/api](https://sntlhq.com/api)
253
+ - **Free IP lookup tool** — [sntlhq.com/ip-lookup](https://sntlhq.com/ip-lookup)
254
+
255
+ ## License
256
+
257
+ MIT © [Sentinel Edge Networks LTD](https://sntlhq.com). See [LICENSE](LICENSE).
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
8
  # would install a stranger's code. "sentinelsup" matches the npm scope
9
9
  # (@sentinelsup/sdk).
10
10
  name = "sentinelsup"
11
- version = "0.2.1"
11
+ version = "0.2.2"
12
12
  description = "Sentinel — real-time fraud, VPN, proxy, and bot detection API. Free tier, sub-40ms response."
13
13
  readme = "README.md"
14
14
  requires-python = ">=3.8"
@@ -46,6 +46,10 @@ class EvaluateResult:
46
46
  network: Dict[str, Any] = field(default_factory=dict)
47
47
  device: Dict[str, Any] = field(default_factory=dict)
48
48
  reasons: list = field(default_factory=list)
49
+ email: Optional[Dict[str, Any]] = None
50
+ decision_source: Optional[str] = None
51
+ engine_decision: Optional[str] = None
52
+ test: bool = False
49
53
  raw: Dict[str, Any] = field(default_factory=dict)
50
54
 
51
55
  @property
@@ -166,6 +170,10 @@ class Sentinel:
166
170
  network=data.get("network") or {},
167
171
  device=data.get("device") or {},
168
172
  reasons=data.get("reasons") or [],
173
+ email=data.get("email"),
174
+ decision_source=data.get("decision_source"),
175
+ engine_decision=data.get("engine_decision"),
176
+ test=bool(data.get("test")),
169
177
  raw=data,
170
178
  )
171
179
 
File without changes
File without changes
File without changes