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.
- sentinelsup-0.2.2/PKG-INFO +286 -0
- sentinelsup-0.2.2/README.md +257 -0
- {sentinelsup-0.1.0 → sentinelsup-0.2.2}/pyproject.toml +1 -1
- {sentinelsup-0.1.0 → sentinelsup-0.2.2}/sentinel/__init__.py +91 -39
- sentinelsup-0.2.2/sentinel/py.typed +0 -0
- sentinelsup-0.1.0/PKG-INFO +0 -177
- sentinelsup-0.1.0/README.md +0 -148
- {sentinelsup-0.1.0 → sentinelsup-0.2.2}/.gitignore +0 -0
- {sentinelsup-0.1.0 → sentinelsup-0.2.2}/LICENSE +0 -0
- {sentinelsup-0.1.0 → sentinelsup-0.2.2}/examples/django_middleware.py +0 -0
- {sentinelsup-0.1.0 → sentinelsup-0.2.2}/examples/flask_signup_guard.py +0 -0
|
@@ -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
|
+
[](https://pypi.org/project/sentinelsup/)
|
|
35
|
+
[](https://pypi.org/project/sentinelsup/)
|
|
36
|
+
[](./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
|
+
[](https://pypi.org/project/sentinelsup/)
|
|
6
|
+
[](https://pypi.org/project/sentinelsup/)
|
|
7
|
+
[](./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.
|
|
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(
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
87
|
+
def _request(
|
|
84
88
|
self,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
) ->
|
|
88
|
-
"""
|
|
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}
|
|
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
|
-
|
|
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
|
sentinelsup-0.1.0/PKG-INFO
DELETED
|
@@ -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).
|
sentinelsup-0.1.0/README.md
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|