The Deterministic Trust Layer
Sentinel Engine is a high-velocity decision engine that renders sub-50ms trust decisions for your API traffic. It replaces user-hostile CAPTCHAs with infrastructure forensics and cryptographic Proof-of-Work — blocking automated attacks without ever interrupting a legitimate user.
Cloudflare Turnstile is for browsers. Sentinel is for APIs.
Outcome-Based Security
We measure success in blocked automation and reduced fraud, not traffic volume. Sentinel protects your bottom line by reducing infrastructure costs and preventing revenue loss from ghost traffic.
Core Architecture
Sentinel operates on a Zero-Friction, Zero-Trust philosophy using three distinct layers of defense:
1. Fast-Path Matrix (< 50ms)
Incoming requests are instantly checked against an in-memory matrix of known-bad hosting providers, proxy networks, and datacenter IP blocks. The majority of primitive bot traffic is blocked here without any external database lookup.
2. Behavioral Work Tokens (BWT)
When an IP is flagged as "Unstable" (ambiguous signals), Sentinel issues a cryptographic challenge. Legitimate clients solve it via the Sentinel Widget while their browser computes a SHA-256 Proof-of-Work in the background. Primitive scripts fail to produce a valid solution.
3. Infrastructure Forensics
For high-sensitivity endpoints, Sentinel performs real-time analysis: headless browser detection (Puppeteer, Playwright), VPN/Tor masking, automated runtime signatures, and request velocity anomalies.
Verdict Reference
Every Sentinel response contains a verdict field. Here is what each value means and what action to take:
| Verdict | Meaning | Your Action |
|---|---|---|
TRUSTED | Clean residential IP, no risk signals. | Allow the request. |
UNSTABLE | Ambiguous signals (shared ISP, high velocity). | Serve the widget challenge. |
UNTRUSTED | VPN, datacenter, known-bad infrastructure. | Hard block (403) or serve the widget depending on your policy. |
The Verification Lifecycle
When your API decides a challenge is required, the flow is always the same three steps:
1. Challenge Issuance — Your backend calls /v1/challenge/issue (authenticated with your site key) to obtain a cryptographic nonce tied to the user's IP.
2. Intent demonstration — Show one Sentinel widget. Hold (widget.js) asks the user to press and hold. Click (widget-click.js) asks for one click. Either widget solves the same SHA-256 proof in the browser.
3. Token Issuance — Your backend calls /v1/challenge/verify. If the nonce is valid, Sentinel returns a trust_token. Pass this as x-sentinel-trust on the retried request. The backend checks it and allows through.
Public Pre-check Endpoint
GET /v1/precheck is a public hint: should you show a widget? It does not use your saved policy or a trust token. The decision you enforce is POST /v2/evaluate. See Where Sentinel applies.
GET /v1/precheckForward the real client IP via x-forwarded-for. The response tells you whether to show the widget or proceed immediately:
{
"required": true,
"verdict": "UNTRUSTED",
"score": 5,
"target": "104.21.0.1",
"trace_id": "a1b2c3d4"
}required: false→ clean IP, proceed with your action immediately.required: true→ show the widget before allowing the action.
Testing
You can mock any IP for local development using the query parameter: GET /v1/precheck?mock_ip=104.21.0.1
Frontend Integration
Option A: The Visual Widget (Recommended)
Load the widget script directly from the Sentinel origin. The widget resolves its API base URL from its own src, so self-hosting it will break challenge requests.
Use one widget. Both accept the same data-sitekey, fire the same sentinelSuccess event, and write the same sentinel-token field.
Hold — press and hold:
<div id="sentinel-widget" data-sitekey="sl_your_site_key_here"></div>
<script src="https://sentinel.risksignal.name.ng/widget.js" async defer></script>Click — one click:
<div id="sentinel-click-widget" data-sitekey="sl_your_site_key_here"></div>
<script src="https://sentinel.risksignal.name.ng/widget-click.js" async defer></script>Key format
Site keys always start with sl_. Do not use your billing API key (which starts with sl_ but is longer) as the site key — they are different credentials. Get your site key from the Sentinel Dashboard.
When verification completes, either widget fires a sentinelSuccess event on document. Listen for it to get the trust token. The examples below use the hold widget. For click, swap in sentinel-click-widget and widget-click.js. The event, the hidden field, and the header do not change.
document.addEventListener('sentinelSuccess', (event) => {
const trustToken = event.detail.trust_token;
// Retry your API call with the token attached
await fetch('/api/your-endpoint', {
method: 'POST',
headers: {
'x-sentinel-trust': trustToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ /* your payload */ }),
});
});If the widget is inside a <form>, the token is also automatically injected as a hidden <input name="sentinel-token"> field — no extra code needed for traditional form submissions.
SPA Integration (React / Vue / Svelte)
There are two distinct patterns for integrating Sentinel into a modern SPA. Choose the pattern that best fits your application's architecture.
Pattern 1: Optimistic Action (Reactive)
Best for: Low-frequency, high-value actions (login, checkout, single form submissions).
Flow: Try the action → Catch 401 → Show Widget → Solve → Retry Action.
This approach assumes the user is trusted. It only unhides the widget if your backend denies the request with a 401 Challenge Required.
1. Include the hidden widget container in your UI:
<div id="sentinel-global-modal" style="display: none;">
<div id="sentinel-widget" data-sitekey="sl_your_site_key_here"></div>
</div>2. Implement the try/catch logic:
async function doAction(payload, token = null) {
const headers = { 'Content-Type': 'application/json' };
if (token) headers['x-sentinel-trust'] = token;
const res = await fetch('/api/your-endpoint', {
method: 'POST',
headers,
body: JSON.stringify(payload),
});
// 1. Backend evaluates the IP. Unstable IP without a token gets a 401.
if (res.status === 401) {
const data = await res.json();
if (data.action_required === 'solve_bwt') {
// 2. Unhide widget to force verification
document.getElementById('sentinel-global-modal').style.display = 'flex';
// 3. Wait for user to hold the button
document.addEventListener('sentinelSuccess', async (event) => {
document.getElementById('sentinel-global-modal').style.display = 'none';
// Retry the exact same action with the token
await doAction(payload, event.detail.trust_token);
}, { once: true });
return;
}
}
// Handle success...
}Pattern 2: Proactive Precheck (Interactive Apps)
Best for: Complex dashboards, interactive tools (e.g., node graphs), and any application where the user performs multiple actions in rapid succession.
Flow: Page Load → /v1/precheck → Show Widget if risky → Store Token → User interacts normally.
If a user naturally performs many quick actions (like expanding nodes on a graph), Pattern 1 would trigger velocity rate limits, mistaking the user for a script. Pattern 2 avoids this by quietly verifying the IP before the user clicks anything, and keeping the resulting token in state.
1. Run Precheck on Mount:
import { useEffect, useRef, useState } from 'react';
function Dashboard() {
const [challengeRequired, setChallengeRequired] = useState(false);
const trustTokenRef = useRef(null);
// Silently check user IP on page load
useEffect(() => {
fetch('https://sentinel.risksignal.name.ng/v1/precheck')
.then(res => res.json())
.then(data => {
if (data.required) setChallengeRequired(true); // Risky IP, show widget
})
.catch(() => {}); // Fail open
}, []);
// Listen for the widget solve
useEffect(() => {
const handler = (e) => {
trustTokenRef.current = e.detail.trust_token; // Store token for future actions
setChallengeRequired(false); // Hide widget
};
document.addEventListener('sentinelSuccess', handler);
return () => document.removeEventListener('sentinelSuccess', handler);
}, []);
// Use the token proactively on actions
const handleAction = async () => {
const headers = { 'Content-Type': 'application/json' };
if (trustTokenRef.current) {
headers['x-sentinel-trust'] = trustTokenRef.current;
}
await fetch('/api/secure-action', { method: 'POST', headers });
};
return (
<>
<div id="sentinel-global-modal" style={{ display: challengeRequired ? 'flex' : 'none' }}>
<div id="sentinel-widget" data-sitekey="sl_..."/>
</div>
<button onClick={handleAction}>Execute Action</button>
</>
);
}Backend Integration
Supabase Edge Functions (Deno)
Call /v1/precheck and forward the real client IP. Do not call /v2/evaluate for gateway enforcement — that endpoint requires a billing API key and will return 403 for site keys, silently passing all traffic through.
async function sentinelGate(req: Request): Promise<Response | null> {
const ip =
req.headers.get('cf-connecting-ip') ||
(req.headers.get('x-forwarded-for') || '').split(',')[0].trim() ||
'127.0.0.1';
const trustToken = req.headers.get('x-sentinel-trust');
// If caller already has a valid trust token, let them through immediately.
if (trustToken) return null;
const res = await fetch('https://sentinel.risksignal.name.ng/v1/precheck', {
method: 'GET',
headers: { 'x-forwarded-for': ip },
});
if (!res.ok) return null; // Fail open — never block on Sentinel downtime
const trust = await res.json();
if (trust.required) {
if (trust.verdict === 'UNTRUSTED') {
return new Response(
JSON.stringify({ error: 'Infrastructure Denied.' }),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
);
}
// UNSTABLE — issue a challenge
return new Response(
JSON.stringify({ action_required: 'solve_bwt', error: 'Challenge Required' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
return null; // Trusted — let the request proceed
}
// Usage in your serve handler:
serve(async (req) => {
const block = await sentinelGate(req);
if (block) return block;
// ... your actual handler logic
});Node.js / Express
Use the api-turnstile npm package. Install it and import it as follows:
npm install api-turnstileimport { sentinel } from 'api-turnstile';
import express from 'express';
const app = express();
app.use('/api', sentinel({
apiKey: process.env.SENTINEL_API_KEY, // your sl_... API key
onBlock: (req, res) => {
res.status(403).json({ error: 'Infrastructure blocked' });
},
onChallenge: (req, res) => {
res.status(401).json({ action_required: 'solve_bwt', error: 'Challenge Required' });
},
}));The middleware automatically reads x-sentinel-trust from incoming headers and bypasses the challenge flow if a valid token is present.
Cloudflare Workers / Vercel Edge
For edge deployments where you need sub-10ms performance and zero dependencies, you can natively verify traffic using the V2 Evaluate endpoint. The engine evaluates both the IP and the Token sequentially, rendering a verdict instantly.
export default {
async fetch(request, env, ctx) {
const ip =
request.headers.get('cf-connecting-ip') ||
request.headers.get('x-forwarded-for') ||
'0.0.0.0';
const trustToken = request.headers.get('x-sentinel-trust');
// 1. Send the metadata AND the Token directly to the V2 Engine
try {
const edgeQuery = await fetch('https://sentinel.risksignal.name.ng/v2/evaluate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.SENTINEL_API_KEY}`,
'Content-Type': 'application/json',
...(trustToken ? { 'x-sentinel-trust': trustToken } : {})
},
body: JSON.stringify({ target: ip })
});
if (edgeQuery.ok) {
const { decision } = await edgeQuery.json();
// 2. Enforce the verdict natively at the edge
if (decision.verdict === 'BLOCK') {
return new Response('Infrastructure Denied', { status: 403 });
}
if (decision.verdict === 'CHALLENGE') {
return new Response(JSON.stringify({ action_required: 'solve_bwt' }), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
}
} catch(e) {
// Sentinel timeout — gracefully fallback to origin
}
// 3. Trusted or allowed via valid cryptographic token — pass to your origin
return fetch(request);
},
};This guarantees that user tokens are crypto-verified natively by the Sentinel V2 API, preventing any client-side spoofing, without needing to import any V1 legacy libraries.
Fail-Open Design
Every integration above follows the fail-open principle: if Sentinel is unreachable (timeout, network error), the gate returns null / proceeds normally. This ensures your service stays online even if Sentinel has an outage. Never block legitimate traffic because of a dependency failure.
Trust Token Reference
A trust token is a base64-encoded HMAC string issued by /v1/challenge/verify after a successful widget solve. It is:
- IP-bound — tied to the exact IP that solved the challenge.
- Time-limited — expires 30 minutes after issuance (
expires_in: 1800). - Header-delivered — send it as
x-sentinel-trust: <token>on subsequent requests.
Pass it through as-is. You do not need to decode or validate it on your end — Sentinel's backend verifies the HMAC on every evaluation.