sentinelsup 0.1.0__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.
@@ -0,0 +1,286 @@
1
+ Metadata-Version: 2.4
2
+ Name: sentinelsup
3
+ Version: 0.2.2
4
+ Summary: Sentinel — real-time fraud, VPN, proxy, and bot detection API. Free tier, sub-40ms response.
5
+ Project-URL: Homepage, https://sntlhq.com
6
+ Project-URL: Documentation, https://sntlhq.com/api
7
+ Project-URL: Repository, https://github.com/sentinelsup/sentinel-python
8
+ Project-URL: Issues, https://github.com/sentinelsup/sentinel-python/issues
9
+ Author-email: Sentinel Edge Networks LTD <support@sntlhq.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: antidetect-browser,api,bot-detection,device-fingerprinting,fraud-detection,proxy-detection,sentinel,vpn-detection
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Topic :: Security
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.8
28
+ Description-Content-Type: text/markdown
29
+
30
+ # sentinelsup — Sentinel Python SDK
31
+
32
+ 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.
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/sentinelsup.svg)](https://pypi.org/project/sentinelsup/)
35
+ [![Python versions](https://img.shields.io/pypi/pyversions/sentinelsup.svg)](https://pypi.org/project/sentinelsup/)
36
+ [![license](https://img.shields.io/pypi/l/sentinelsup.svg)](./LICENSE)
37
+
38
+ Zero dependencies — just the standard library. Works with Flask, Django, FastAPI, or bare `urllib`.
39
+
40
+ ## Set up with AI (fastest)
41
+
42
+ Using Claude Code, Cursor, Copilot, or any AI coding assistant? Paste this one
43
+ prompt and it wires the whole integration — frontend script, backend check,
44
+ env var, and a test:
45
+
46
+ > Fetch https://sntlhq.com/integrate.md and follow it to add Sentinel fraud
47
+ > protection to this app — protect signup, login, and checkout. My API key
48
+ > is sk_live_YOUR_KEY; put it in a SENTINEL_KEY env var, never in
49
+ > client-side code. Then show me how to test it.
50
+
51
+ [`integrate.md`](https://sntlhq.com/integrate.md) is the canonical
52
+ machine-readable integration guide, kept in sync with the live API.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install sentinelsup
58
+ ```
59
+
60
+ Python 3.8+. Get a free API key (no credit card) at [sntlhq.com/signup](https://sntlhq.com/signup).
61
+
62
+ ## Quick start
63
+
64
+ ```python
65
+ import os
66
+ from sentinel import Sentinel
67
+
68
+ s = Sentinel(api_key=os.environ["SENTINEL_KEY"]) # or omit — reads the env var itself
69
+
70
+ result = s.evaluate(token=request.json["sentinelToken"]) # token from the frontend SDK
71
+
72
+ if result.is_blocked: # decision == 'block'
73
+ abort(403)
74
+
75
+ print(result.decision) # 'allow' | 'review' | 'block' — route on this
76
+ print(result.risk_score) # 0..100
77
+ print(result.network) # {'vpn': True, 'proxy': False, 'datacenter': True, ...}
78
+ print(result.reasons) # ['vpn_detected', 'datacenter_asn', ...]
79
+ ```
80
+
81
+ Check the signup email against the disposable-domain feed (checked
82
+ transiently, never stored), or look up an arbitrary IP with no browser
83
+ token at all:
84
+
85
+ ```python
86
+ result = s.evaluate(token=tok, email=data["email"])
87
+ if result.raw.get("email", {}).get("disposable"):
88
+ ... # burner domain — decision is escalated allow → review
89
+
90
+ info = s.lookup("185.220.101.34") # GET /v1/lookup/{ip} — same key & quota
91
+ print(info["verdict"]) # 'allow' | 'review' | 'block'
92
+ print(info["signals"]) # {'vpn': ..., 'proxied': ..., 'tor': ..., 'dch': ..., 'anon': ...}
93
+ ```
94
+
95
+ ## What you get back
96
+
97
+ `evaluate()` returns an `EvaluateResult` dataclass:
98
+
99
+ ```python
100
+ @dataclass
101
+ class EvaluateResult:
102
+ decision: str | None # 'allow' | 'review' | 'block'
103
+ risk_score: int | None # 0..100
104
+ ip: str | None
105
+ country: str | None # ISO-2
106
+ network: dict # {vpn, proxy, datacenter, anonymous, tor, residential, service}
107
+ device: dict # antidetect / automation / emulator signals
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
+ raw: dict # full upstream response
114
+
115
+ is_suspicious: bool # True if decision != 'allow'
116
+ is_blocked: bool # True if decision == 'block'
117
+ ```
118
+
119
+ Try the live sample (same shape, no key needed):
120
+
121
+ ```bash
122
+ curl "https://sntlhq.com/v1/evaluate/sample?scenario=vpn"
123
+ ```
124
+
125
+ Or use the [interactive playground](https://sntlhq.com/api#playground).
126
+
127
+ ## Frontend setup
128
+
129
+ Add the Sentinel SDK to your frontend. One script loads **both** layers —
130
+ network (VPN/proxy/datacenter) and device (antidetect/bot/tampering):
131
+
132
+ ```html
133
+ <script async src="https://sntlhq.com/assets/sentinel.js"></script>
134
+
135
+ <!-- Add class="monocle-enriched" to any form you want evaluated -->
136
+ <form class="monocle-enriched" id="signup-form">
137
+ <!-- The SDK injects both:
138
+ <input type="hidden" name="monocle" value="eyJ..."> (network)
139
+ <input type="hidden" name="sentinel_fp" value="a1b2..."> (device) -->
140
+ </form>
141
+ ```
142
+
143
+ Forward both fields to your backend with the form submission and pass them to
144
+ `evaluate()` as `token` and `fingerprint_event_id` — without the second one,
145
+ the device-layer signals (antidetect, automation, emulator) never fire. For
146
+ fetch/XHR submissions, collect them explicitly:
147
+
148
+ ```js
149
+ const { token, fingerprintEventId } = await window.Sentinel.collect();
150
+ ```
151
+
152
+ ## Examples
153
+
154
+ ### Flask — block VPN/proxy signups
155
+
156
+ ```python
157
+ from flask import Flask, request, abort, jsonify
158
+ from sentinel import Sentinel, SentinelError
159
+
160
+ app = Flask(__name__)
161
+ sentinel = Sentinel() # reads SENTINEL_KEY (or SENTINEL_API_KEY) from env
162
+
163
+ @app.route("/signup", methods=["POST"])
164
+ def signup():
165
+ data = request.get_json()
166
+ try:
167
+ result = sentinel.evaluate(token=data["sentinelToken"])
168
+ except SentinelError as e:
169
+ # Fail open OR fail closed — your call. Logged either way.
170
+ app.logger.warning("Sentinel error: %s", e)
171
+ result = None
172
+
173
+ if result and result.is_blocked:
174
+ abort(403, "Signup blocked")
175
+
176
+ # ... your normal signup flow
177
+ return jsonify({"ok": True})
178
+ ```
179
+
180
+ ### Django — middleware for high-value endpoints
181
+
182
+ ```python
183
+ from django.http import JsonResponse
184
+ from sentinel import Sentinel
185
+
186
+ sentinel = Sentinel() # reads SENTINEL_KEY (or SENTINEL_API_KEY) from env
187
+
188
+ class FraudCheckMiddleware:
189
+ def __init__(self, get_response):
190
+ self.get_response = get_response
191
+
192
+ def __call__(self, request):
193
+ if request.path.startswith("/api/checkout"):
194
+ token = request.META.get("HTTP_X_SENTINEL_TOKEN")
195
+ if token:
196
+ try:
197
+ result = sentinel.evaluate(token=token)
198
+ if result.is_blocked:
199
+ return JsonResponse({"error": "blocked"}, status=403)
200
+ except Exception:
201
+ pass # fail open
202
+ return self.get_response(request)
203
+ ```
204
+
205
+ Runnable versions live in [`examples/`](./examples/).
206
+
207
+ ## API
208
+
209
+ ### `Sentinel(api_key=None, endpoint="https://sntlhq.com", timeout=5.0)`
210
+
211
+ | Option | Default | Description |
212
+ |--------|---------|-------------|
213
+ | `api_key` | `$SENTINEL_KEY` (falls back to `$SENTINEL_API_KEY`) | Your key starting with `sk_live_` |
214
+ | `endpoint` | `https://sntlhq.com` | Override base URL (for testing) |
215
+ | `timeout` | `5.0` | Per-request timeout in seconds |
216
+
217
+ ### `sentinel.evaluate(token, fingerprint_event_id=None, account_id=None, email=None)`
218
+
219
+ Returns `EvaluateResult`. Raises `SentinelError` on network/API failure.
220
+
221
+ - `fingerprint_event_id` — adds the `device` signal block (antidetect, automation, emulator, …).
222
+ - `account_id` — your own user id for this session; enables multi-accounting detection (`device.linked_accounts` / `device.multi_account`).
223
+ - `email` — adds `email.disposable` to the raw response; burner domains escalate `allow` to `review`.
224
+
225
+ ### `sentinel.lookup(ip)`
226
+
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
+
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
+
247
+ ## Errors
248
+
249
+ All failures raise `SentinelError`. The exception carries `.status` (HTTP code) and `.body` (parsed error body) when available.
250
+
251
+ ```python
252
+ from sentinel import Sentinel, SentinelError
253
+
254
+ try:
255
+ result = sentinel.evaluate(token=tok)
256
+ except SentinelError as e:
257
+ if e.status == 429:
258
+ pass # back off
259
+ elif e.status and 400 <= e.status < 500:
260
+ pass # bad input, won't recover by retrying
261
+ else:
262
+ pass # transient — retry once or fail open
263
+ ```
264
+
265
+ ## Rate limits
266
+
267
+ Free tier: **1,000 requests/hour** per API key. No monthly cap, no credit
268
+ card. Upgrade at [sntlhq.com](https://sntlhq.com) when you need more.
269
+
270
+ ## What Sentinel detects
271
+
272
+ VPNs (commercial + self-hosted) · residential proxies (Bright Data, IPRoyal,
273
+ and similar networks) · datacenter IPs · Tor exit nodes · antidetect browsers
274
+ (Kameleo, GoLogin, Multilogin, Dolphin{anty}, AdsPower) · headless browsers
275
+ and automation (Puppeteer, Playwright, Selenium) · AI agents · emulators and
276
+ virtual machines · browser tampering.
277
+
278
+ ## Related
279
+
280
+ - **Node.js SDK** — [`@sentinelsup/sdk`](https://github.com/sentinelsup/sentinel-node) on npm
281
+ - **API docs** — [sntlhq.com/api](https://sntlhq.com/api)
282
+ - **Free IP lookup tool** — [sntlhq.com/ip-lookup](https://sntlhq.com/ip-lookup)
283
+
284
+ ## License
285
+
286
+ MIT © [Sentinel Edge Networks LTD](https://sntlhq.com). See [LICENSE](LICENSE).
@@ -0,0 +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
+ ## 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.1.0"
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"
@@ -4,9 +4,9 @@ fraud detection API at https://sntlhq.com/v1/evaluate.
4
4
 
5
5
  Usage:
6
6
  from sentinel import Sentinel
7
- s = Sentinel(api_key=os.environ["SENTINEL_KEY"])
7
+ s = Sentinel() # reads SENTINEL_KEY or SENTINEL_API_KEY from the env
8
8
  result = s.evaluate(token=request.json["sentinelToken"])
9
- if result.is_suspicious:
9
+ if result.is_blocked:
10
10
  abort(403)
11
11
  """
12
12
 
@@ -14,11 +14,11 @@ import json
14
14
  import os
15
15
  from dataclasses import dataclass, field
16
16
  from typing import Any, Dict, Optional
17
- from urllib import error, request
17
+ from urllib import error, parse, request
18
18
 
19
19
  DEFAULT_ENDPOINT = "https://sntlhq.com"
20
20
  DEFAULT_TIMEOUT = 5.0
21
- __version__ = "0.1.0"
21
+ __version__ = "0.2.1"
22
22
 
23
23
 
24
24
  class SentinelError(Exception):
@@ -43,11 +43,13 @@ class EvaluateResult:
43
43
  risk_score: Optional[int] = None
44
44
  ip: Optional[str] = None
45
45
  country: Optional[str] = None
46
- asn: Optional[str] = None
47
- asn_org: Optional[str] = None
48
46
  network: Dict[str, Any] = field(default_factory=dict)
49
47
  device: Dict[str, Any] = field(default_factory=dict)
50
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
51
53
  raw: Dict[str, Any] = field(default_factory=dict)
52
54
 
53
55
  @property
@@ -69,48 +71,30 @@ class Sentinel:
69
71
  endpoint: str = DEFAULT_ENDPOINT,
70
72
  timeout: float = DEFAULT_TIMEOUT,
71
73
  ) -> None:
72
- api_key = api_key or os.environ.get("SENTINEL_API_KEY")
74
+ # SENTINEL_KEY is the name every doc surface uses; SENTINEL_API_KEY
75
+ # is kept for existing installs that already adopted it.
76
+ api_key = api_key or os.environ.get("SENTINEL_KEY") or os.environ.get("SENTINEL_API_KEY")
73
77
  if not api_key or not isinstance(api_key, str):
74
78
  raise SentinelError(
75
79
  "Sentinel: api_key is required. "
76
- "Pass it explicitly or set SENTINEL_API_KEY. "
80
+ "Pass it explicitly or set SENTINEL_KEY. "
77
81
  "Get one free at https://sntlhq.com/signup"
78
82
  )
79
83
  self.api_key = api_key
80
84
  self.endpoint = endpoint.rstrip("/")
81
85
  self.timeout = timeout
82
86
 
83
- def evaluate(
87
+ def _request(
84
88
  self,
85
- token: str,
86
- fingerprint_event_id: Optional[str] = None,
87
- ) -> EvaluateResult:
88
- """Evaluate a visitor session for fraud signals.
89
-
90
- Args:
91
- token: Sentinel client-side token from the frontend SDK.
92
- fingerprint_event_id: Optional Fingerprint event id for device signals.
93
-
94
- Returns:
95
- EvaluateResult with decision, risk_score, network, device, and reasons.
96
-
97
- Raises:
98
- SentinelError: on network failure, timeout, or non-2xx response.
99
- """
100
- if not token or not isinstance(token, str):
101
- raise SentinelError(
102
- "Sentinel.evaluate: token (client-side Sentinel token) is required"
103
- )
104
-
105
- payload: Dict[str, Any] = {"token": token}
106
- if fingerprint_event_id:
107
- payload["fingerprintEventId"] = fingerprint_event_id
108
-
109
- body = json.dumps(payload).encode("utf-8")
89
+ path: str,
90
+ payload: Optional[Dict[str, Any]] = None,
91
+ ) -> Dict[str, Any]:
92
+ """Shared transport: auth header, timeout, JSON parsing, error mapping."""
93
+ body = json.dumps(payload).encode("utf-8") if payload is not None else None
110
94
  req = request.Request(
111
- f"{self.endpoint}/v1/evaluate",
95
+ f"{self.endpoint}{path}",
112
96
  data=body,
113
- method="POST",
97
+ method="POST" if payload is not None else "GET",
114
98
  headers={
115
99
  "Authorization": f"Bearer {self.api_key}",
116
100
  "Content-Type": "application/json",
@@ -121,7 +105,7 @@ class Sentinel:
121
105
  try:
122
106
  with request.urlopen(req, timeout=self.timeout) as resp:
123
107
  raw = resp.read().decode("utf-8")
124
- data = json.loads(raw) if raw else {}
108
+ return json.loads(raw) if raw else {}
125
109
  except error.HTTPError as e:
126
110
  try:
127
111
  err_body = json.loads(e.read().decode("utf-8"))
@@ -139,18 +123,86 @@ class Sentinel:
139
123
  except Exception as e:
140
124
  raise SentinelError(f"Sentinel: unexpected error — {e}") from None
141
125
 
126
+ def evaluate(
127
+ self,
128
+ token: str,
129
+ fingerprint_event_id: Optional[str] = None,
130
+ account_id: Optional[str] = None,
131
+ email: Optional[str] = None,
132
+ ) -> EvaluateResult:
133
+ """Evaluate a visitor session for fraud signals.
134
+
135
+ Args:
136
+ token: Sentinel client-side token from the frontend SDK.
137
+ fingerprint_event_id: Optional Fingerprint event id for device signals.
138
+ account_id: Optional account/user id — enables multi-accounting
139
+ detection (device.linked_accounts / device.multi_account).
140
+ email: Optional signup email. Adds ``email.disposable`` to the raw
141
+ response (burner domains escalate allow to review). Checked
142
+ transiently — never stored or logged.
143
+
144
+ Returns:
145
+ EvaluateResult with decision, risk_score, network, device, and reasons.
146
+
147
+ Raises:
148
+ SentinelError: on network failure, timeout, or non-2xx response.
149
+ """
150
+ if not token or not isinstance(token, str):
151
+ raise SentinelError(
152
+ "Sentinel.evaluate: token (client-side Sentinel token) is required"
153
+ )
154
+
155
+ payload: Dict[str, Any] = {"token": token}
156
+ if fingerprint_event_id:
157
+ payload["fingerprintEventId"] = fingerprint_event_id
158
+ if account_id:
159
+ payload["accountId"] = account_id
160
+ if email:
161
+ payload["email"] = email
162
+
163
+ data = self._request("/v1/evaluate", payload)
164
+
142
165
  return EvaluateResult(
143
166
  decision=data.get("decision"),
144
167
  risk_score=data.get("risk_score"),
145
168
  ip=data.get("ip"),
146
169
  country=data.get("country"),
147
- asn=data.get("asn"),
148
- asn_org=data.get("asn_org"),
149
170
  network=data.get("network") or {},
150
171
  device=data.get("device") or {},
151
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")),
152
177
  raw=data,
153
178
  )
154
179
 
180
+ def lookup(self, ip: str) -> Dict[str, Any]:
181
+ """Look up an arbitrary public IP address — no browser token needed.
182
+
183
+ Wraps ``GET /v1/lookup/{ip}``. Useful for batch scoring, log
184
+ enrichment, and server-side screening. Shares the per-key hourly
185
+ quota with :meth:`evaluate`.
186
+
187
+ Args:
188
+ ip: Public IPv4 or IPv6 address, e.g. ``"185.220.101.34"``.
189
+
190
+ Returns:
191
+ The raw response dict: ``verdict`` ('allow' | 'review' | 'block'),
192
+ ``risk_score`` (0-100), ``known``, ``signals``
193
+ ({vpn, proxied, tor, dch, anon} or None), ``network``
194
+ ({asn, org, country, city}), ``latency_ms``. Note ``known: False``
195
+ means our feeds hold no data for the IP — it is NOT a clean
196
+ guarantee.
197
+
198
+ Raises:
199
+ SentinelError: on network failure, timeout, or non-2xx response.
200
+ """
201
+ if not ip or not isinstance(ip, str):
202
+ raise SentinelError(
203
+ "Sentinel.lookup: ip (public IPv4 or IPv6 address) is required"
204
+ )
205
+ return self._request(f"/v1/lookup/{parse.quote(ip.strip(), safe='')}")
206
+
155
207
 
156
208
  __all__ = ["Sentinel", "SentinelError", "EvaluateResult", "__version__"]
File without changes
@@ -1,177 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: sentinelsup
3
- Version: 0.1.0
4
- Summary: Sentinel — real-time fraud, VPN, proxy, and bot detection API. Free tier, sub-40ms response.
5
- Project-URL: Homepage, https://sntlhq.com
6
- Project-URL: Documentation, https://sntlhq.com/api
7
- Project-URL: Repository, https://github.com/sentinelsup/sentinel-python
8
- Project-URL: Issues, https://github.com/sentinelsup/sentinel-python/issues
9
- Author-email: Sentinel Edge Networks LTD <support@sntlhq.com>
10
- License: MIT
11
- License-File: LICENSE
12
- Keywords: antidetect-browser,api,bot-detection,device-fingerprinting,fraud-detection,proxy-detection,sentinel,vpn-detection
13
- Classifier: Development Status :: 4 - Beta
14
- Classifier: Intended Audience :: Developers
15
- Classifier: License :: OSI Approved :: MIT License
16
- Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3 :: Only
18
- Classifier: Programming Language :: Python :: 3.8
19
- Classifier: Programming Language :: Python :: 3.9
20
- Classifier: Programming Language :: Python :: 3.10
21
- Classifier: Programming Language :: Python :: 3.11
22
- Classifier: Programming Language :: Python :: 3.12
23
- Classifier: Programming Language :: Python :: 3.13
24
- Classifier: Topic :: Internet :: WWW/HTTP
25
- Classifier: Topic :: Security
26
- Classifier: Typing :: Typed
27
- Requires-Python: >=3.8
28
- Description-Content-Type: text/markdown
29
-
30
- # Sentinel Python SDK
31
-
32
- Real-time fraud, VPN, proxy, and bot detection — free tier, sub-40ms global response.
33
-
34
- Zero dependencies. Just the standard library.
35
-
36
- ## Install
37
-
38
- ```bash
39
- pip install sentinelsup
40
- ```
41
-
42
- ## Quick start
43
-
44
- ```python
45
- import os
46
- from sentinel import Sentinel
47
-
48
- s = Sentinel(api_key=os.environ["SENTINEL_API_KEY"])
49
-
50
- result = s.evaluate(token=request.json["sentinelToken"])
51
-
52
- if result.is_suspicious:
53
- return abort(403, "Sentinel flagged this session")
54
-
55
- print(result.decision) # 'allow' | 'review' | 'block'
56
- print(result.risk_score) # 0..100
57
- print(result.network) # {'vpn': True, 'proxy': False, 'datacenter': True, ...}
58
- print(result.reasons) # ['ip_in_known_vpn_range', 'datacenter_asn', ...]
59
- ```
60
-
61
- ## Get your API key
62
-
63
- Sign up at [sntlhq.com/signup](https://sntlhq.com/signup) — free, no credit card.
64
-
65
- ## Flask example — block VPN/proxy signups
66
-
67
- ```python
68
- from flask import Flask, request, abort, jsonify
69
- from sentinel import Sentinel, SentinelError
70
-
71
- app = Flask(__name__)
72
- sentinel = Sentinel() # reads SENTINEL_API_KEY from env
73
-
74
- @app.route("/signup", methods=["POST"])
75
- def signup():
76
- data = request.get_json()
77
- try:
78
- result = sentinel.evaluate(token=data["sentinelToken"])
79
- except SentinelError as e:
80
- # Fail open OR fail closed — your call. Logged either way.
81
- app.logger.warning("Sentinel error: %s", e)
82
- result = None
83
-
84
- if result and result.is_blocked:
85
- abort(403, "Signup blocked")
86
-
87
- # ... your normal signup flow
88
- return jsonify({"ok": True})
89
- ```
90
-
91
- ## Django example — middleware for high-value endpoints
92
-
93
- ```python
94
- from django.http import JsonResponse
95
- from sentinel import Sentinel
96
-
97
- sentinel = Sentinel() # reads SENTINEL_API_KEY from env
98
-
99
- class FraudCheckMiddleware:
100
- def __init__(self, get_response):
101
- self.get_response = get_response
102
-
103
- def __call__(self, request):
104
- if request.path.startswith("/api/checkout"):
105
- token = request.META.get("HTTP_X_SENTINEL_TOKEN")
106
- if token:
107
- try:
108
- result = sentinel.evaluate(token=token)
109
- if result.is_blocked:
110
- return JsonResponse({"error": "blocked"}, status=403)
111
- except Exception:
112
- pass # fail open
113
- return self.get_response(request)
114
- ```
115
-
116
- ## Configuration
117
-
118
- ```python
119
- Sentinel(
120
- api_key="sk_live_...", # required (or via SENTINEL_API_KEY env var)
121
- endpoint="https://sntlhq.com", # override for testing
122
- timeout=5.0, # seconds
123
- )
124
- ```
125
-
126
- ## Response shape
127
-
128
- ```python
129
- @dataclass
130
- class EvaluateResult:
131
- decision: str | None # 'allow' | 'review' | 'block'
132
- risk_score: int | None # 0..100
133
- ip: str | None
134
- country: str | None # ISO-2
135
- asn: str | None # 'AS16509'
136
- asn_org: str | None # 'Amazon.com, Inc.'
137
- network: dict # {vpn, proxy, datacenter, anonymous, residential}
138
- device: dict # {headless, automation, fingerprint_age_days}
139
- reasons: list[str]
140
- raw: dict # full upstream response
141
-
142
- is_suspicious: bool # True if decision != 'allow'
143
- is_blocked: bool # True if decision == 'block'
144
- ```
145
-
146
- ## Errors
147
-
148
- All failures raise `SentinelError`. The exception carries `.status` (HTTP code) and `.body` (parsed error body) when available.
149
-
150
- ```python
151
- from sentinel import Sentinel, SentinelError
152
-
153
- try:
154
- result = sentinel.evaluate(token=tok)
155
- except SentinelError as e:
156
- if e.status == 429:
157
- # back off
158
- pass
159
- elif e.status and 400 <= e.status < 500:
160
- # bad input, won't recover by retrying
161
- pass
162
- else:
163
- # transient — retry once or fail open
164
- pass
165
- ```
166
-
167
- ## Try the API without signing up
168
-
169
- ```bash
170
- curl https://sntlhq.com/v1/evaluate/sample?scenario=vpn
171
- ```
172
-
173
- Or use the [interactive playground](https://sntlhq.com/api#playground).
174
-
175
- ## License
176
-
177
- MIT. See [LICENSE](LICENSE).
@@ -1,148 +0,0 @@
1
- # Sentinel Python SDK
2
-
3
- Real-time fraud, VPN, proxy, and bot detection — free tier, sub-40ms global response.
4
-
5
- Zero dependencies. Just the standard library.
6
-
7
- ## Install
8
-
9
- ```bash
10
- pip install sentinelsup
11
- ```
12
-
13
- ## Quick start
14
-
15
- ```python
16
- import os
17
- from sentinel import Sentinel
18
-
19
- s = Sentinel(api_key=os.environ["SENTINEL_API_KEY"])
20
-
21
- result = s.evaluate(token=request.json["sentinelToken"])
22
-
23
- if result.is_suspicious:
24
- return abort(403, "Sentinel flagged this session")
25
-
26
- print(result.decision) # 'allow' | 'review' | 'block'
27
- print(result.risk_score) # 0..100
28
- print(result.network) # {'vpn': True, 'proxy': False, 'datacenter': True, ...}
29
- print(result.reasons) # ['ip_in_known_vpn_range', 'datacenter_asn', ...]
30
- ```
31
-
32
- ## Get your API key
33
-
34
- Sign up at [sntlhq.com/signup](https://sntlhq.com/signup) — free, no credit card.
35
-
36
- ## Flask example — block VPN/proxy signups
37
-
38
- ```python
39
- from flask import Flask, request, abort, jsonify
40
- from sentinel import Sentinel, SentinelError
41
-
42
- app = Flask(__name__)
43
- sentinel = Sentinel() # reads SENTINEL_API_KEY from env
44
-
45
- @app.route("/signup", methods=["POST"])
46
- def signup():
47
- data = request.get_json()
48
- try:
49
- result = sentinel.evaluate(token=data["sentinelToken"])
50
- except SentinelError as e:
51
- # Fail open OR fail closed — your call. Logged either way.
52
- app.logger.warning("Sentinel error: %s", e)
53
- result = None
54
-
55
- if result and result.is_blocked:
56
- abort(403, "Signup blocked")
57
-
58
- # ... your normal signup flow
59
- return jsonify({"ok": True})
60
- ```
61
-
62
- ## Django example — middleware for high-value endpoints
63
-
64
- ```python
65
- from django.http import JsonResponse
66
- from sentinel import Sentinel
67
-
68
- sentinel = Sentinel() # reads SENTINEL_API_KEY from env
69
-
70
- class FraudCheckMiddleware:
71
- def __init__(self, get_response):
72
- self.get_response = get_response
73
-
74
- def __call__(self, request):
75
- if request.path.startswith("/api/checkout"):
76
- token = request.META.get("HTTP_X_SENTINEL_TOKEN")
77
- if token:
78
- try:
79
- result = sentinel.evaluate(token=token)
80
- if result.is_blocked:
81
- return JsonResponse({"error": "blocked"}, status=403)
82
- except Exception:
83
- pass # fail open
84
- return self.get_response(request)
85
- ```
86
-
87
- ## Configuration
88
-
89
- ```python
90
- Sentinel(
91
- api_key="sk_live_...", # required (or via SENTINEL_API_KEY env var)
92
- endpoint="https://sntlhq.com", # override for testing
93
- timeout=5.0, # seconds
94
- )
95
- ```
96
-
97
- ## Response shape
98
-
99
- ```python
100
- @dataclass
101
- class EvaluateResult:
102
- decision: str | None # 'allow' | 'review' | 'block'
103
- risk_score: int | None # 0..100
104
- ip: str | None
105
- country: str | None # ISO-2
106
- asn: str | None # 'AS16509'
107
- asn_org: str | None # 'Amazon.com, Inc.'
108
- network: dict # {vpn, proxy, datacenter, anonymous, residential}
109
- device: dict # {headless, automation, fingerprint_age_days}
110
- reasons: list[str]
111
- raw: dict # full upstream response
112
-
113
- is_suspicious: bool # True if decision != 'allow'
114
- is_blocked: bool # True if decision == 'block'
115
- ```
116
-
117
- ## Errors
118
-
119
- All failures raise `SentinelError`. The exception carries `.status` (HTTP code) and `.body` (parsed error body) when available.
120
-
121
- ```python
122
- from sentinel import Sentinel, SentinelError
123
-
124
- try:
125
- result = sentinel.evaluate(token=tok)
126
- except SentinelError as e:
127
- if e.status == 429:
128
- # back off
129
- pass
130
- elif e.status and 400 <= e.status < 500:
131
- # bad input, won't recover by retrying
132
- pass
133
- else:
134
- # transient — retry once or fail open
135
- pass
136
- ```
137
-
138
- ## Try the API without signing up
139
-
140
- ```bash
141
- curl https://sntlhq.com/v1/evaluate/sample?scenario=vpn
142
- ```
143
-
144
- Or use the [interactive playground](https://sntlhq.com/api#playground).
145
-
146
- ## License
147
-
148
- MIT. See [LICENSE](LICENSE).
File without changes
File without changes