EN

Theme

Accent colour

Your theme and colour carry over to satelit.md and the app.

Dashboard Choose a plan

Satelit Shield, from the script to the server.

The base is https://api.satelit.md/shield/v1. The page uses the site's public key; your server sends the secret in Authorization: Bearer shs_….

  1. Add the site and get the keys

    In the dashboard you add the site and its domains. You get the site key, public (shp_ and 24 characters), and the secret (shs_ and 40), which you see once.

    Protect your siteProtect your site

  2. Put the sensor on your pages

    Direct or through a first-party proxy. A new site starts with every rule in shadow (shadow).

    <!-- in every page, direct -->
    <script src="https://api.satelit.md/shield/v1/sensor.js"
      data-site="shp_XXXXXXXXXXXXXXXXXXXXXXXX" async></script>
    # Caddyfile: /_sh/* goes to Shield, with your secret and the visitor's address
    example.md {
    	handle_path /_sh/* {
    		rewrite * /shield/v1{uri}
    		reverse_proxy https://api.satelit.md {
    			header_up Host {upstream_hostport}
    			header_up X-Shield-Secret {env.SHIELD_SECRET}
    			header_up X-Shield-Client-IP {client_ip}
    			header_up -Cookie
    			header_up -Authorization
    		}
    	}
    	# ... the rest of your site
    }
    
    # in every page:
    # <script src="/_sh/sensor.js" data-site="shp_XXXXXXXXXXXXXXXXXXXXXXXX" data-base="/_sh" async></script>
    # nginx: /_sh/ goes to Shield, with your secret and the visitor's address
    location /_sh/ {
        proxy_pass https://api.satelit.md/shield/v1/;
        proxy_ssl_server_name on;
        proxy_set_header Host api.satelit.md;
        proxy_set_header X-Shield-Secret "shs_...";        # keep this file out of your repository
        proxy_set_header X-Shield-Client-IP $remote_addr;  # behind Cloudflare: $http_cf_connecting_ip
        proxy_set_header Cookie "";
        proxy_set_header Authorization "";
    }
    
    # in every page:
    # <script src="/_sh/sensor.js" data-site="shp_XXXXXXXXXXXXXXXXXXXXXXXX" data-base="/_sh" async></script>
  3. Read the events, then switch protection on

    In the dashboard you see what each rule would have stopped. Switch to enforce the rules you trust.

The script's attributes

AttributeWhat it does
data-siteRequired. The site's public key.
data-baseWhere the sensor sends its reports. By default: the folder the script was loaded from; through a first-party proxy, /_sh.
data-overlay="0"0: the sensor never draws the check in the page; your server serves the check page.
HTML
<!-- in every page, direct -->
<script src="https://api.satelit.md/shield/v1/sensor.js"
  data-site="shp_XXXXXXXXXXXXXXXXXXXXXXXX" async></script>

First-party proxy

Your server sends /_sh/* to https://api.satelit.md/shield/v1/*, adds X-Shield-Secret and X-Shield-Client-IP (and, if you like, X-Shield-Client-Country) and passes User-Agent, Accept-Language, Sec-Fetch-* and Sec-CH-UA* unchanged. The client address is believed only together with the right secret. Your visitors' cookies should not go to Shield.

# Caddyfile: /_sh/* goes to Shield, with your secret and the visitor's address
example.md {
	handle_path /_sh/* {
		rewrite * /shield/v1{uri}
		reverse_proxy https://api.satelit.md {
			header_up Host {upstream_hostport}
			header_up X-Shield-Secret {env.SHIELD_SECRET}
			header_up X-Shield-Client-IP {client_ip}
			header_up -Cookie
			header_up -Authorization
		}
	}
	# ... the rest of your site
}

# in every page:
# <script src="/_sh/sensor.js" data-site="shp_XXXXXXXXXXXXXXXXXXXXXXXX" data-base="/_sh" async></script>
# nginx: /_sh/ goes to Shield, with your secret and the visitor's address
location /_sh/ {
    proxy_pass https://api.satelit.md/shield/v1/;
    proxy_ssl_server_name on;
    proxy_set_header Host api.satelit.md;
    proxy_set_header X-Shield-Secret "shs_...";        # keep this file out of your repository
    proxy_set_header X-Shield-Client-IP $remote_addr;  # behind Cloudflare: $http_cf_connecting_ip
    proxy_set_header Cookie "";
    proxy_set_header Authorization "";
}

# in every page:
# <script src="/_sh/sensor.js" data-site="shp_XXXXXXXXXXXXXXXXXXXXXXXX" data-base="/_sh" async></script>

The JavaScript API

  • SatelitShield.ready(fn): fn({a, t, explain}) after the page's first report. a: '' nothing, 'g' a check, 'b' stopped.
  • SatelitShield.token(fn): fn(token) with a fresh verdict token, for a form or an API call. You verify it on the server.
  • SatelitShield.version: the sensor's version.
  • The satelit-shield event on document, with the same detail as ready.
JavaScript
// after the page's first report
SatelitShield.ready(function (r) {
  // r.a: "" nothing, "g" a check, "b" blocked; r.t: a verdict token
});
document.addEventListener("satelit-shield", function (e) {
  console.log(e.detail.a, e.detail.t);
});

// a fresh token goes with the form, in a hidden field
form.addEventListener("submit", function (e) {
  e.preventDefault();
  SatelitShield.token(function (token) {
    form.elements.shield_token.value = token;
    form.submit();
  });
});

The check page and the stop page

The page's mode comes from the data-shield attribute of the html element. No attribute: a normal page. check: your check page, with status 429; the sensor solves it, keeps the pass and reloads the same address. ban: the page with status 403; the sensor only translates it.

429
<!doctype html>
<html lang="ro" data-shield="check">
<head>
  <meta charset="utf-8">
  <meta content="noindex" name="robots">
  <script src="/_sh/sensor.js" data-site="shp_XXXXXXXXXXXXXXXXXXXXXXXX" data-base="/_sh" async></script>
</head>
<body><p>Se verifică browserul.</p></body>
</html>

The cookies

  • __shpThe pass after a solved check: p1.{exp}.{token}.{sig}.
  • __shs1: the browser ran the sensor (400 days), so a returning reader never counts as having no proof.

Both are first-party, on your domain: SameSite=Lax, Secure on https, path /.

The pass, checked on your side

A pass is checked on your own server, with no call to Shield.

  • The pass key: HMAC-SHA256(key = sha256_hex(secret), "satelit-shield-pass-v1"), in hex. Compute it once.
  • sig is the first 22 characters of base64url(HMAC-SHA256(pass key, "p1|{exp}|{token}|{client key}|{User-Agent}")); exp in unix seconds, token 16 hex characters.
  • A pass is valid while exp has not passed and the signature matches this request's client key and User-Agent: a pass copied to another address or browser opens nothing.
  • Life: 12 hours; 2 hours for risky clients (Tor, a hosting network, a fleet, a hard decision in the last 7 days). A pass opens a check, never a stop.
// Satelit Shield: check a pass (the __shp cookie) on your own server, with no call to Shield. Node 18+.
// A pass is p1.{exp}.{token}.{sig}; sig is the first 22 characters of
// base64url(HMAC-SHA256(pass key, "p1|{exp}|{token}|{client key}|{User-Agent}")).
import { createHash, createHmac, timingSafeEqual } from "node:crypto";

// The pass key: HMAC-SHA256 with the key sha256_hex(secret) over "satelit-shield-pass-v1", in hex.
export function passKey(secret) {
  const key = createHash("sha256").update(secret).digest("hex");
  return createHmac("sha256", key).update("satelit-shield-pass-v1").digest("hex");
}

// The client key: an IPv4 address as it is, an IPv6 address as its /64 (the first four groups and "::").
export function clientKey(ip) {
  const v4 = /^(?:::ffff:)?(\d{1,3}(?:\.\d{1,3}){3})$/i.exec(ip);
  if (v4) return v4[1];
  const [head, tail = ""] = ip.toLowerCase().split("%")[0].split("::");
  const a = head ? head.split(":") : [];
  const b = tail ? tail.split(":") : [];
  const groups = [...a, ...Array(Math.max(0, 8 - a.length - b.length)).fill("0"), ...b];
  return groups.slice(0, 4).map((g) => parseInt(g, 16).toString(16)).join(":") + "::";
}

// True when the pass has not expired and was signed for this address and this browser.
export function passValid(pass, secret, ip, userAgent, now = Date.now() / 1000) {
  const m = /^p1\.(\d+)\.([0-9a-f]{16})\.([A-Za-z0-9_-]{22})$/.exec(pass ?? "");
  if (!m || Number(m[1]) <= now) return false;
  const message = `p1|${m[1]}|${m[2]}|${clientKey(ip)}|${userAgent}`;
  const sig = createHmac("sha256", passKey(secret)).update(message).digest("base64url").slice(0, 22);
  return timingSafeEqual(Buffer.from(sig), Buffer.from(m[3]));
}
"""Satelit Shield: check a pass (the __shp cookie) on your own server, with no call to Shield. Python 3.8+.

A pass is p1.{exp}.{token}.{sig}; sig is the first 22 characters of
base64url(HMAC-SHA256(pass key, "p1|{exp}|{token}|{client key}|{User-Agent}")).
"""
import base64
import hashlib
import hmac
import ipaddress
import re
import time

PASS = re.compile(r"p1\.(\d+)\.([0-9a-f]{16})\.([A-Za-z0-9_-]{22})")


def pass_key(secret):
    """HMAC-SHA256 with the key sha256_hex(secret) over "satelit-shield-pass-v1", in hex."""
    key = hashlib.sha256(secret.encode()).hexdigest().encode()
    return hmac.new(key, b"satelit-shield-pass-v1", hashlib.sha256).hexdigest()


def client_key(ip):
    """An IPv4 address as it is, an IPv6 address as its /64 (the first four groups and "::")."""
    a = ipaddress.ip_address(ip.split("%")[0])
    if a.version == 6 and a.ipv4_mapped:
        a = a.ipv4_mapped
    if a.version == 4:
        return str(a)
    return ":".join(format(int(g, 16), "x") for g in a.exploded.split(":")[:4]) + "::"


def pass_valid(value, secret, ip, user_agent, now=None):
    """True when the pass has not expired and was signed for this address and this browser."""
    m = PASS.fullmatch(value or "")
    if not m or int(m[1]) <= (time.time() if now is None else now):
        return False
    message = f"p1|{m[1]}|{m[2]}|{client_key(ip)}|{user_agent}".encode()
    mac = hmac.new(pass_key(secret).encode(), message, hashlib.sha256).digest()
    sig = base64.urlsafe_b64encode(mac).decode().rstrip("=")[:22]
    return hmac.compare_digest(sig, m[3])

The client key

An IPv4 address as it is; for IPv6, the /64 network, written as its first four groups followed by :: (2a01:4f8:221:1f15::). Stops, checks, passes and counters are per client key.

Server calls

All of them need Authorization: Bearer shs_… and are made from your server only.

POST /verify

Verifies a verdict token. A token lives 300 seconds and verifies once; otherwise success is false, with error: used, expired, invalid or other_ip. With ip, a token from another client key is refused. score: from 0, a person, to 100, a certain bot.

POST /verify
curl https://api.satelit.md/shield/v1/verify \
  -H "Authorization: Bearer $SHIELD_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
  "token": "<the verdict token>",
  "ip": "203.0.113.9",
  "ua": "Mozilla/5.0 ..."
}'
200
{
  "ok": true,
  "success": true,
  "score": 3,
  "verdict": "human",
  "action": "allow",
  "rules": [],
  "signals": {
    "hard": [],
    "soft": [],
    "st": []
  },
  "crawler": null,
  "issued_at": "2026-11-05T21:00:00Z",
  "age_s": 4
}

POST /assess

One request, synchronous. challenge: serve the check page (429); block: 403. A decision in shadow answers allow, with would. The request counts toward the rules exactly like one /ingest line.

POST /assess
curl https://api.satelit.md/shield/v1/assess \
  -H "Authorization: Bearer $SHIELD_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
  "ip": "203.0.113.9",
  "ua": "Mozilla/5.0 ...",
  "method": "GET",
  "path": "/ro/catalog/",
  "host": "example.md",
  "kind": "html",
  "headers": {
    "sec-fetch-site": "none",
    "sec-ch-ua": "...",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\"",
    "accept-language": "ro-RO,ro;q=0.9",
    "referer": ""
  },
  "pass": "",
  "seen": true
}'
200
{
  "action": "allow",
  "rule": "",
  "mode": "",
  "would": "",
  "crawler": null,
  "key": "203.0.113.9",
  "until": null
}

POST /ingest

Batches of at most 500 lines, without waiting for the answer. k: html a page, api a JSON call, asset a file. pass: 0 none, 1 valid, 2 present but invalid. seen: the __shs cookie. g: 1 when the answer was the check page.

POST /ingest
curl https://api.satelit.md/shield/v1/ingest \
  -H "Authorization: Bearer $SHIELD_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
  "lines": [
    {
      "t": 1762369200000,
      "ip": "203.0.113.9",
      "ua": "Mozilla/5.0 ...",
      "m": "GET",
      "p": "/ro/catalog/",
      "s": 200,
      "k": "html",
      "h": {
        "sfs": "none",
        "chua": "...",
        "chm": "?0",
        "chp": "\"Windows\"",
        "al": "ro-RO",
        "ref": ""
      },
      "pass": 1,
      "seen": 1,
      "g": 0
    }
  ]
}'
200
{
  "ok": true,
  "n": 1,
  "decided": 0
}

GET /decisions?since=0

since=0, or no since, gives the whole active list; with the cursor you got, only the changes. a: b a stop (403), g a check (429 without a valid pass), - lifted. Shadow decisions never appear here. Read the list every 15–30 seconds and keep it in memory.

GET /decisions?since=0
curl "https://api.satelit.md/shield/v1/decisions?since=0" \
  -H "Authorization: Bearer $SHIELD_SECRET"
200
{
  "cursor": "1234",
  "full": true,
  "items": [
    {
      "key": "203.0.113.9",
      "a": "b",
      "rule": "automation",
      "until": "2026-11-06T21:00:00Z"
    },
    {
      "key": "2a01:4f8:1:2::",
      "a": "g",
      "rule": "no-proof",
      "until": "2026-11-06T21:00:00Z"
    },
    {
      "key": "198.51.100.4",
      "a": "-",
      "rule": "",
      "until": null
    }
  ]
}

GET /config

The site's settings: modes, thresholds, the trap path, the pass life, the plan and the month's use.

GET /config
curl "https://api.satelit.md/shield/v1/config" \
  -H "Authorization: Bearer $SHIELD_SECRET"
200
{
  "site": "shp_XXXXXXXXXXXXXXXXXXXXXXXX",
  "name": "example.md",
  "modes": {
    "automation": "enforce",
    "no-proof": "shadow"
  },
  "thresholds": {},
  "trap": "/_sh/t/9f3k2",
  "overlay": true,
  "pass_life": 43200,
  "plan": "shield_pro",
  "quota": {
    "month": "2026-11",
    "used": 1234,
    "limit": 2500000
  }
}

POST /trust

A protected address for a number of days: hard rules only show the check. For a paying member or an office.

POST /trust
curl https://api.satelit.md/shield/v1/trust \
  -H "Authorization: Bearer $SHIELD_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
  "ip": "203.0.113.9",
  "days": 30
}'
200
{
  "ok": true,
  "key": "203.0.113.9",
  "until": "2026-12-05T21:00:00+00:00"
}

GET /events?limit=50 · GET /stats?days=7

The same rows as the dashboard: the events, filtered by rule and action, and the figures by day.

GET /events?limit=50
curl "https://api.satelit.md/shield/v1/events?limit=50" \
  -H "Authorization: Bearer $SHIELD_SECRET"
200
{
  "items": [
    {
      "id": 81,
      "at": "2026-11-05T21:04:00Z",
      "key": "198.51.100.7",
      "rule": "automation",
      "action": "ban",
      "mode": "enforce",
      "ua": "Mozilla/5.0 ... HeadlessChrome/141.0.0.0 ...",
      "evidence": {
        "hard": [
          "wd"
        ]
      }
    }
  ]
}

Verify the token on forms

On a vote, a comment or a sign-up, the page asks for a token with SatelitShield.token() and sends it with the form. Your server verifies it once, within 300 seconds.

curl https://api.satelit.md/shield/v1/verify \
  -H "Authorization: Bearer $SHIELD_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "token": "<the token from the form>",
    "ip": "203.0.113.9"
  }'
// Node 18+: the token came with the form, ip is the visitor's address
async function fromPerson(token, ip) {
  const res = await fetch("https://api.satelit.md/shield/v1/verify", {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.SHIELD_SECRET}`, "Content-Type": "application/json" },
    body: JSON.stringify({ token, ip }),
  });
  const v = await res.json();
  return v.success === true && v.action !== "block";
}
import json, os, urllib.request

def from_person(token, ip):
    req = urllib.request.Request(
        "https://api.satelit.md/shield/v1/verify",
        data=json.dumps({"token": token, "ip": ip}).encode(),
        headers={"Authorization": f"Bearer {os.environ['SHIELD_SECRET']}", "Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=5) as res:
        v = json.load(res)
    return v.get("success") is True and v.get("action") != "block"
<?php
function from_person(string $token, string $ip): bool {
    $ch = curl_init("https://api.satelit.md/shield/v1/verify");
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_TIMEOUT => 5,
        CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SHIELD_SECRET"), "Content-Type: application/json"],
        CURLOPT_POSTFIELDS => json_encode(["token" => $token, "ip" => $ip]),
    ]);
    $v = json_decode(curl_exec($ch), true);
    curl_close($ch);
    return ($v["success"] ?? false) === true && ($v["action"] ?? "") !== "block";
}

A middleware in Node

The decisions in memory, the requests to /ingest in batches, the pass checked locally. It runs as it is, with SHIELD_SECRET and SHIELD_SITE in the environment.

Optional, for the whole site. A middleware keeps the decisions in memory, reads them from /decisions every 15–30 seconds and sends the requests to /ingest in batches. No visitor's request waits for the network.

shield-middleware.mjs
// Satelit Shield in a Node server: Express or any (req, res, next) stack. Node 18+, no packages.
// The decisions stay in memory and are read again every 20 s, so a visitor's request never waits for the network.
// Every request is sent to /ingest in batches. /_sh/ (the sensor) is served by the proxy in front, step 1.
import { clientKey, passValid } from "./shield-pass.mjs";

const API = process.env.SHIELD_API || "https://api.satelit.md/shield/v1";
const SECRET = process.env.SHIELD_SECRET;   // shs_..., never in a page
const SITE = process.env.SHIELD_SITE;       // shp_..., the public site key
const HEADERS = { Authorization: `Bearer ${SECRET}`, "Content-Type": "application/json" };
const decisions = new Map();                // client key -> { a: "b" | "g", rule, until }
const lines = [];
let cursor = "0";

export async function poll() {
  const res = await fetch(`${API}/decisions?since=${cursor}`, { headers: HEADERS });
  if (!res.ok) return;
  const body = await res.json();
  if (body.full) decisions.clear();
  for (const d of body.items) {
    if (d.a === "-") decisions.delete(d.key);
    else decisions.set(d.key, d);
  }
  cursor = body.cursor;
}

export async function flush() {
  while (lines.length) {
    const batch = lines.splice(0, 500);
    await fetch(`${API}/ingest`, { method: "POST", headers: HEADERS, body: JSON.stringify({ lines: batch }) }).catch(() => {});
  }
}

export function start() {
  const quiet = (job) => () => job().catch(() => {});
  quiet(poll)();
  setInterval(quiet(poll), 20_000).unref();
  setInterval(quiet(flush), 5_000).unref();
}

const cookie = (req, name) => new RegExp(`(?:^|;\\s*)${name}=([^;]*)`).exec(req.headers.cookie ?? "")?.[1] ?? "";
const page = (mode, text) => `<!doctype html><html data-shield="${mode}"><head><meta charset="utf-8">`
  + `<meta content="noindex" name="robots"><meta name="viewport" content="width=device-width">`
  + `<script src="/_sh/sensor.js" data-site="${SITE}" data-base="/_sh" async></script></head><body><p>${text}</p></body></html>`;

export function shield(req, res, next) {
  const ip = req.ip ?? req.socket.remoteAddress;   // behind a proxy: Express's "trust proxy" gives the visitor's address
  const ua = req.headers["user-agent"] ?? "";
  const path = (req.originalUrl ?? req.url).split("?")[0];
  const shp = cookie(req, "__shp");
  const pass = !shp ? 0 : passValid(shp, SECRET, ip, ua) ? 1 : 2;
  const d = decisions.get(clientKey(ip));
  const on = d && (!d.until || Date.parse(d.until) > Date.now()) ? d.a : "";
  // a ban is a 403; a check is a 429 unless the browser holds a valid pass (a pass never opens a ban)
  const gate = on === "b" ? 403 : on === "g" && pass !== 1 ? 429 : 0;
  res.on("finish", () => {
    const type = String(res.getHeader("content-type") ?? "");
    const kind = /\.(css|js|mjs|png|jpe?g|gif|svg|webp|avif|ico|woff2?)$/i.test(path) ? "asset" : /json/.test(type) ? "api" : "html";
    const h = (name) => req.headers[name] ?? "";
    lines.push({
      t: Date.now(), ip, ua, m: req.method, p: path, s: res.statusCode, k: kind,
      h: { sfs: h("sec-fetch-site"), chua: h("sec-ch-ua"), chm: h("sec-ch-ua-mobile"), chp: h("sec-ch-ua-platform"), al: h("accept-language"), ref: h("referer") },
      pass, seen: cookie(req, "__shs") === "1" ? 1 : 0, g: gate === 429 ? 1 : 0,
    });
  });
  if (!gate) return next();
  res.statusCode = gate;
  res.setHeader("Content-Type", "text/html; charset=utf-8");
  res.setHeader("Cache-Control", "no-store");
  res.end(gate === 429 ? page("check", "Checking your browser.") : page("ban", "Access blocked."));
}

Errors

A wrong call answers with error.code and error.message.

CodeWhen
unauthorizedThe secret is missing or wrong.
forbidden_originThe page is not on one of the site's domains (or a subdomain).
invalidThe request does not have the shape on this page.
quotaThe month's checks are used up. The sensor never breaks the page: it answers as if there were nothing to do.
busyShield is busy. Try again in a few seconds.
not_foundThe site or the resource does not exist.
401
{
  "error": {
    "code": "unauthorized",
    "message": "The secret is missing or wrong"
  }
}

The rules

Each rule has its own mode: off, shadow or enforce.

Hard rules stop an unprotected address. Soft ones only show the check. A rule that goes past 50 stops an hour, or a site past 200, falls back to shadow by itself, and you get an event.

  • automation hardsensor

    WebDriver, ChromeDriver, Selenium, PhantomJS, Nightmare, Playwright, Puppeteer, Cypress, TestCafe, HeadlessChrome.

  • stealth hardsensor

    Patched automation: a replaced Function.toString, a window from a detached iframe, a redefined webdriver, Chromium posing as Chrome, a worker that says HeadlessChrome.

  • antidetect hard or softsensor

    A spoofed fingerprint: a JS engine older than the claimed browser, canvas noise, a screen that differs from CSS, time zone, language, missing OS fonts, a worker or Client Hints that disagree.

  • fake-crawler hardboth

    A crawler name refuted by two independent sources: the official IP list and reverse DNS.

  • robots hardrequests

    A self-declared bot asking for paths its own robots.txt group disallows.

  • trap hardrequests

    The site's hidden trap path, disallowed in robots.txt.

  • challenge-ignored hardrequests

    The check page served 6 or more times to one address, with no answer.

  • browser-soft softsensor

    Two or more weak hints: no plugins on desktop Chrome, no window.chrome, empty languages, software WebGL, the DevTools protocol.

  • no-proof softrequests

    12 or more pages in 10 minutes, with no sensor report and no __shs cookie.

  • rate softrequests

    60 or more pages a minute.

  • walk softrequests

    40 or more different pages in 10 minutes.

  • stale-browser softrequests

    Desktop Chrome 8 or more versions behind, with no pass and no __shs.

  • ua-claim softrequests

    Chrome 101+ with a full build number (real Chrome writes N.0.0.0), or no Sec-Fetch-Site on Chrome 80+.

  • client-hints softrequests

    The Sec-CH-UA headers contradict the User-Agent: mobile, platform, version, or missing on Chrome 90+.

  • datacenter softboth

    A browser User-Agent from a hosting or cloud network (ASN), with no pass.

Never judged
Verified crawlers and your allowlist.
A check only, never a stop
Tor exits, shared addresses (3 or more real browsers on one address within 24 hours) and the addresses you trust.

What Shield keeps

  • Check IDs only. The sensor sends the check IDs, interaction counts (no positions or keys) and the browser's first language. No fingerprint hash.
  • Cookies on your domain only. __shp, the pass after a check, and __shs, the sign that the browser ran the sensor. Satelit sets no cookie of its own.
  • Short retention. The client key (the address or its /64 network), the User-Agent, the rule and its evidence: 30 days. Hourly counters: 90 days.
  • The secret stays on your server. The site key is public. You see the secret once, and Satelit keeps only its hash.

About Satelit Shield