EN

Theme

Accent colour

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

Dashboard Choose a plan

Satelit Shield

Stop bots and scrapers, not people

Shield recognises automation, antidetect browsers and scrapers, in the page and on the server; the most a person sees is a check that passes by itself in about a second.

The first 7 days are a trial, with 100,000 checks.

The real sensor

Your browser, as Shield sees it

Checking your browser

The Shield sensor runs on this page, as on a customer's site.

A real browserscore …/100

No sign of automation or of a spoofed fingerprint. On a protected site, pages open as usual.

A few risk signsscore …/100

Weak signs, not certain ones. On a protected site you would see at most the check that passes by itself.

Automation foundscore …/100

Certain signs of automation or disguise. On a protected site, access would be stopped.

A search crawlerscore …/100

A declared crawler. Verified ones, such as Google and Bing, are never stopped.

Your browser passed

Shield asked for no check from this browser.

The test did not answer

A content blocker or a slow network stopped the script. Through a first-party proxy it comes from your own domain.

What the sensor saw

The signs appear after the sensor's first report.

These are the check IDs as the sensor sends them. What each one means is in the Shield dashboard.

The same script you put on your site. It sends check IDs only: no positions, keys or text.

  • The check passes by itself in about a second. A person clicks nothing.
  • Watch first: see what Shield would stop, then switch protection on.
  • Verified search crawlers are never touched.

What it stops

Automation, false fingerprints, scrapers and abuse

Each card says what Shield looks for and the rules that decide.

  • Browser automation

    Selenium, Puppeteer, Playwright, Cypress, TestCafe and headless Chrome, recognised by the traces they leave in the page.

    automation

  • Stealth plugins and “undetected” builds

    Automation patched to look human. Shield looks for the patches themselves: replaced functions, detached windows, a Chromium posing as Chrome.

    stealth

  • Antidetect browsers

    Spoofed fingerprints. Shield compares what the browser claims with what its engine does: the screen, the time zone, the language, the fonts, the canvas drawing.

    antidetectbrowser-soft

  • HTML scrapers

    Programs that ask for pages without running them: no sensor report, too many pages, too fast.

    no-proofratewalk

  • Fake Googlebot

    A crawler name is believed only when the official IP list and reverse DNS confirm it. Verified crawlers always get through.

    fake-crawlerrobotstrap

  • Address fleets and data centres

    Browsers from hosting networks, User-Agents that lie, headers that do not match. IPv6 is counted per /64 network.

    datacenterua-claimclient-hintsstale-browser

  • Fake votes, comments and sign-ups

    The form sends a sensor token and your server verifies it once, within 300 seconds. Verified with the visitor's address, a token copied to another address is refused.

    POST /verify

    The answer of POST /verify
    {
      "success": true,
      "verdict": "human",
      "score": 3,
      "action": "allow",
      "age_s": 4
    }

How it works

Two layers, one decision

The sensor sees the browser, your server sees the requests. Shield puts them together for each address and decides once.

  1. In the page

    The sensor

    • One ES5 script on your pages, loaded async: it never holds the page back.
    • It sends check IDs only: no positions, keys, text or fingerprint hash.
    • It solves the check page: a 14-bit proof of work.
    • It keeps the pass for 12 hours, in a cookie on your domain.
  2. On the server

    The requests

    • Your server or edge tells Shield which requests arrive: one at a time to /assess, in batches to /ingest.
    • Shield counts per address: IPv4, or the /64 network for IPv6.
    • A crawler name is checked against the official IP list and by reverse DNS.
    • The Sec-Fetch, Client Hints and User-Agent headers have to say the same thing.

Three possible outcomes

  1. 200 Through People and verified crawlers. Nothing changes.
  2. 429 A one-second check Weak signs: a page that passes by itself. The person clicks nothing.
  3. 403 Stopped Certain signs: 24 hours, then 7 days, then 30 days on a repeat within 90 days.

Watch first, then protect

Every rule has a mode: off, shadow (watching: Shield only notes what it would do) or enforce (protection). A new site starts with every rule in shadow; you switch them to enforce one at a time.

Verified crawlers always get through

Google, Bing, Apple, Yandex, DuckDuckGo, Baidu, OpenAI, Perplexity, Anthropic and more: no decision, ever. The same for your allowlist.

Install

Three steps, the first is one line

The site key (shp_…) is public and sits in the page. The secret (shs_…) stays on your server; you see it once.

  1. Put the sensor on your pages

    Direct, one line in the page. Recommended: through a first-party proxy, from your own domain. No third-party request, it works under script-src 'self', and ad blockers do not see it.

    <!-- 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>
  2. 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.

    In the page
    // 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();
      });
    });
    POST /shield/v1/verify
    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";
    }
  3. Put the decision in your server

    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.

    // 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."));
    }
    
    // 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]));
    }
    

Rules

15 rules, each with its own mode

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.

9 more rules
  • 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.

Privacy

IDs, not fingerprints

Shield sees what it needs to tell a browser from a program. Nothing more.

  • 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.

Pricing

You pay for checks, not for traffic

A check is one sensor report or one /assess call. Lines sent to /ingest are free, up to 10 for each check.

  1. Shield Start

    One site: a shop, a blog, a small newsroom.

    19 € / month

    381.72 lei

    Sites
    1
    Checks a month
    250,000
    Start with Shield Start Start with Shield Start
  2. Shield Pro

    Recommended

    A few sites with forms, votes or accounts.

    79 € / month

    1,587.15 lei

    Sites
    5
    Checks a month
    2,500,000
    Start with Shield Pro Start with Shield Pro
  3. Shield Business

    Networks of sites and platforms with heavy traffic.

    249 € / month

    5,002.53 lei

    Sites
    25
    Checks a month
    15,000,000
    Start with Shield Business Start with Shield Business
  4. Enterprise

    Large volumes, terms and invoicing by contract.

    from 990 € / month

    from 19,889.59 lei

    Sites
    by contract
    Checks a month
    from 50,000,000
    Book a meeting

Every new site has a 7-day trial, with 100,000 checks. Lei at the BNM rate of 2 November 2026: 1 euro = 20.0905 lei.

Questions about Shield

All of Shield's documentation

Does Shield hurt my Google ranking?

No. Verified crawlers (Google, Bing, Apple, Yandex, DuckDuckGo, Baidu, OpenAI, Perplexity, Anthropic and more) get no decision: their name is confirmed by the official IP list and by reverse DNS. The sensor loads async and never holds the page back.

Does it work with Cloudflare?

Yes. Shield does not take the place of Cloudflare's rules; it adds the layer that sees the browser. If your site is behind Cloudflare, your proxy sends the address from CF-Connecting-IP in X-Shield-Client-IP, so Shield judges the visitor, not Cloudflare.

What happens to a person taken for a bot?

Weak signs only show the check, which passes by itself in about a second. Tor exits, shared addresses and the addresses you trust are never stopped. A rule with too many stops in an hour falls back to shadow by itself, and you can lift any decision from the dashboard.

How do I start without risk?

A new site starts with every rule in shadow: Shield only notes what it would stop. You read the events in the dashboard and switch to enforce the rules you trust, one at a time.

Does it slow my site down?

The sensor loads async, after the page. A middleware keeps the decisions in memory and reads them every 15–30 seconds, so a request never waits for the network. Only /assess, if you use it, is a call per request.

What data does Shield collect?

Check IDs, interaction counts without positions or keys, and the browser's first language. Shield keeps the client key, the User-Agent, the rule and its evidence for 30 days, and hourly counters for 90 days. No fingerprint hash and no Satelit cookies: you can describe it exactly in your privacy policy.

What does Shield do if the script is blocked?

The server layer works without it: a client that asks for many pages with no sensor report gets the check. Through a first-party proxy, the script comes from your own domain and ad blockers do not see it.

How long does a stop last?

24 hours, then 7 days, then 30 days, if the address repeats within 90 days. A valid pass opens a check, never a stop.

See what Shield would stop on your site

A new site starts in shadow: Shield only takes notes. You switch protection on when you are sure, rule by rule.