# Sentinel Integration Guide (for AI coding assistants & developers)

You are integrating **Sentinel** (https://sntlhq.com) — a real-time fraud
detection API — into an existing application. Follow these steps exactly.
This document is the canonical, machine-readable integration reference;
everything in it matches the live API.

If the user has not given you an API key, tell them to grab one free at
https://sntlhq.com/signup (no credit card; key is on the dashboard
immediately) — do not invent a key.

## What you are building

1. Frontend: a script tag that silently collects device/network signals and
   injects a token into marked forms.
2. Backend: on the form's POST handler, send that token to Sentinel's
   `/v1/evaluate` endpoint and allow/review/block based on the response.

Protect the high-value actions first: signup, login, checkout/payment.

## Step 1 — Frontend (any stack)

Add to the `<head>` of every page you want protected:

```html
<script async src="https://sntlhq.com/assets/edge.js" id="_mcl"></script>
```

Add `class="monocle-enriched"` to each form you want evaluated. The SDK then
injects a hidden field automatically:

```html
<form class="monocle-enriched" id="signup-form" method="POST" action="/signup">
  <!-- SDK injects: <input type="hidden" name="monocle" value="eyJ..."> -->
</form>
```

The token arrives at your backend as the `monocle` field of the form POST.
For fetch/XHR submissions, read it yourself and include it in your JSON body:

```js
const token = document.querySelector('input[name="monocle"]').value;
```

For SPAs (React/Vue/Svelte): put the script tag in index.html, give the form
element the `monocle-enriched` class, and read the injected input's value at
submit time.

## Step 2 — Backend

### Node.js (preferred)

```bash
npm install @sentinelsup/sdk
```

```js
const Sentinel = require('@sentinelsup/sdk'); // ESM: import Sentinel from '@sentinelsup/sdk'
const sentinel = new Sentinel({ apiKey: process.env.SENTINEL_KEY });

app.post('/signup', async (req, res) => {
  const result = await sentinel.evaluate({ token: req.body.monocle });
  if (result.decision === 'block') {
    return res.status(403).json({ error: 'Signup blocked for security reasons.' });
  }
  // result.decision === 'review' → let through but flag for manual review,
  // or apply your own policy using result.risk_score (0–100) / result.reasons.
  // ... create the account
});
```

Put the key in the environment, never in client code or the repo:

```
SENTINEL_KEY=sk_live_...
```

### Any other language (raw HTTP)

```
POST https://sntlhq.com/v1/evaluate
Authorization: Bearer sk_live_YOUR_KEY        <-- exactly this header; NOT X-API-Key
Content-Type: application/json

{"token": "<monocle token from the form>"}
```

Python example:

```python
import os, requests

def evaluate(token: str) -> dict:
    r = requests.post(
        "https://sntlhq.com/v1/evaluate",
        headers={"Authorization": f"Bearer {os.environ['SENTINEL_KEY']}"},
        json={"token": token},
        timeout=5,
    )
    r.raise_for_status()
    return r.json()
```

## Step 3 — Response shape (live, verified)

```json
{
  "decision": "review",            // "allow" | "review" | "block"  ← route on this
  "risk_score": 65,                 // 0–100
  "isSuspicious": true,             // simple boolean verdict
  "ip": "198.51.100.18",
  "country": "NL",
  "network": {
    "vpn": true, "proxy": false, "datacenter": true, "anonymous": true,
    "tor": false, "residential": false, "service": "PROTON_VPN"
  },
  "device": {                       // present only when fingerprintEventId was sent
    "antidetect": false, "automation": false, "emulator": false,
    "virtual_machine": false, "incognito": false, "ip_blocklisted": false,
    "visitor_id": "abc123", "tampering_score": 0
  },
  "reasons": ["vpn_detected", "datacenter_asn"],
  "evaluated_in_ms": 28
}
```

Reason codes: `vpn_detected`, `proxy_detected`, `datacenter_asn`,
`tor_exit_node`, `anonymous_network`, `antidetect_browser`,
`automation_detected`, `emulator_detected`, `virtual_machine`,
`ip_blocklisted`, `private_browsing`.

Recommended default policy: hard-fail on `decision === "block"`, soft-flag
on `"review"`. If you only want one boolean, use `isSuspicious`.

## Step 4 — Test it

Without a key (sample endpoint, same shape as production):

```bash
curl "https://sntlhq.com/v1/evaluate/sample?scenario=vpn"      # also: clean, datacenter
```

With a key (a junk token returns 400 "Invalid token." — that proves auth works):

```bash
curl -X POST https://sntlhq.com/v1/evaluate \
  -H "Authorization: Bearer $SENTINEL_KEY" \
  -H "Content-Type: application/json" \
  -d '{"token":"test"}'
```

Then load a protected page in a browser, check the hidden `monocle` input
exists inside the form, submit, and confirm your handler logs a `decision`.

## Failure modes to handle

- **Fail open**: if the Sentinel call times out or errors, let the request
  through (and log it) rather than locking users out. The Node SDK throws
  `SentinelError` — wrap in try/catch.
- **401** = wrong/missing `Authorization: Bearer` header.
- **429** = rate limit (free tier: 1,000 evaluations/hour per key; honor
  `Retry-After`). Fail open on it.
- The hidden `monocle` field can be missing if the script was blocked by an
  ad-blocker — treat a missing token as `review`, not `block`.

## Reference

- Full docs: https://sntlhq.com/api
- Node SDK: https://www.npmjs.com/package/@sentinelsup/sdk (GitHub: https://github.com/sentinelsup/sentinel-node)
- OpenAPI spec: https://sntlhq.com/openapi.json
- Site overview for LLMs: https://sntlhq.com/llms.txt
- Webhooks (threat alerts) + threat log: configured on the dashboard at https://sntlhq.com/dashboard
