Капча, которая останавливает ботов,
а не людей.

VeilGuard проверяет посетителей по невидимым сигналам. Никаких картинок-головоломок, никаких сторонних вызовов. Всё выполняется на вашем собственном сервере.

Без регистрации, без кредитной карты. Установка занимает около двух минут.

example.com/signup

Создание аккаунта

Verified!
VeilGuard

Попробуйте сами

Виджет ниже настоящий: задание, proof-of-work, анализ сигналов и проверка на стороне сервера выполняются прямо на этой странице.

Пример формы регистрации
example.com/signup — veilguard-token

Отправляет поддельный токен с отпечатками автоматизации.

Вердикт сервера
POST verify.php — Токены проверяются локально по HMAC-SHA256. Люди проходят, боты - нет.
Отправьте форму - и увидите, что решил бы ваш сервер.
Что проверяет VeilGuard
  • Фреймворки автоматизации (WebDriver, Puppeteer, Playwright)
  • Headless-браузеры и программный рендеринг
  • User-Agent известных ботов и краулеров
  • Поведенческая энтропия: сигналы мыши, клавиатуры и касаний
  • Proof-of-work, привязанный к одноразовому заданию
  • Анализ таймингов и повторов на вашем сервере

Что делает VeilGuard

Шесть вещей, которые он делает хорошо.

Почти стопроцентная блокировка

Многосигнальный движок объединяет отпечатки автоматизации, поведенческую энтропию, proof-of-work и анализ таймингов на сервере.

Останавливает и AI-ботов

Headless Chrome, автоматизация через CDP и скриптовые агенты вычисляются по отпечаткам и блокируются, а не только наивные краулеры.

Интеграция за 2 минуты

Один сниппет для виджета, один вызов на сервере. Копируйте и вставляйте - для PHP, Node.js, Python и статических сайтов.

Работает целиком на вашем сервере

Проверка - это локальная сверка HMAC-SHA256. Даже если veilguard.online упадёт, ваша защита продолжит работать.

Невидимая по замыслу

Никаких светофоров и пожарных гидрантов. Человек кликает один раз и видит «Проверено!».

Приватность прежде всего и совместимость с GDPR

Никаких cookies, трекинговых пикселей и сторонних запросов. Эфемерные сигналы живут только внутри токена.

Как это работает

Три шага, аккаунт не нужен.

  1. 1.

    Сгенерируйте бесплатные ключи

    Без регистрации и почты. Вы сразу получите ключ сайта и секретный ключ.

  2. 2.

    Вставьте два сниппета

    Скрипт виджета - в форму, вызов проверки - в бэкенд.

  3. 3.

    Готово

    Токены проверяются локально по HMAC-SHA256. Люди проходят, боты - нет.

Интеграция за 2 минуты

Сгенерируйте бесплатные ключи, выберите свой стек, вставьте код. Ничего не уходит с вашего сервера.

1. Сгенерируйте бесплатные ключи
Без регистрации и почты. Вы сразу получите ключ сайта и секретный ключ.
2. Вставьте два сниппета
Скрипт виджета - в форму, вызов проверки - в бэкенд.

1 · Виджет: вставьте в форму

veilguard-widget.js
/*! VeilGuard v1.0.0 - self-hosted bot protection - https://veilguard.online */
(function () {
  'use strict';
  var LINK = 'https://veilguard.online';
  var STR = {
    en: { nb: "I'm not a bot", vd: 'Verified!', ck: 'Verifying…', er: 'Verification failed — click to retry' },
    de: { nb: 'Ich bin kein Bot', vd: 'Verifiziert!', ck: 'Prüfung…', er: 'Fehlgeschlagen — zum Wiederholen klicken' },
    es: { nb: 'No soy un bot', vd: '¡Verificado!', ck: 'Verificando…', er: 'Error — haz clic para reintentar' },
    fr: { nb: "Je ne suis pas un robot", vd: 'Vérifié !', ck: 'Vérification…', er: 'Échec — cliquez pour réessayer' },
    pt: { nb: 'Não sou um bot', vd: 'Verificado!', ck: 'Verificando…', er: 'Falhou — clique para tentar de novo' },
    ru: { nb: 'Я не робот', vd: 'Проверено!', ck: 'Проверка…', er: 'Ошибка — нажмите для повтора' },
    zh: { nb: '我不是机器人', vd: '已验证!', ck: '验证中…', er: '验证失败 — 点击重试' },
    ja: { nb: 'ロボットではありません', vd: '認証済み!', ck: '確認中…', er: '失敗 — クリックで再試行' },
    ar: { nb: 'لست روبوتًا', vd: 'تم التحقق!', ck: 'جارٍ التحقق…', er: 'فشل — انقر للمحاولة مجددًا' }
  };
  var BOT_UA = /bot|crawl|spider|slurp|headless|phantom|selenium|puppeteer|playwright|webdriver|curl|wget|python|scrapy|axios|go-http|httpclient|okhttp|libwww|lighthouse|facebookexternalhit|telegram|whatsapp|discord/i;

  function sha256(ascii) {
    function rightRotate(value, amount) { return (value >>> amount) | (value << (32 - amount)); }
    var mathPow = Math.pow, maxWord = mathPow(2, 32), result = '';
    var words = [], asciiBitLength = ascii.length * 8;
    var hash = sha256.h = sha256.h || [], k = sha256.k = sha256.k || [], primeCounter = k.length;
    var isComposite = {};
    for (var candidate = 2; primeCounter < 64; candidate++) {
      if (!isComposite[candidate]) {
        for (var i = 0; i < 313; i += candidate) { isComposite[i] = candidate; }
        hash[primeCounter] = (mathPow(candidate, 0.5) * maxWord) | 0;
        k[primeCounter++] = (mathPow(candidate, 1 / 3) * maxWord) | 0;
      }
    }
    ascii += '\x80';
    while (ascii.length % 64 - 56) ascii += '\x00';
    for (i = 0; i < ascii.length; i++) {
      var j = ascii.charCodeAt(i);
      if (j >> 8) return '';
      words[i >> 2] |= j << ((3 - i) % 4) * 8;
    }
    words[words.length] = ((asciiBitLength / maxWord) | 0);
    words[words.length] = (asciiBitLength);
    for (j = 0; j < words.length;) {
      var w = words.slice(j, j += 16), oldHash = hash;
      hash = hash.slice(0, 8);
      for (i = 0; i < 64; i++) {
        var w15 = w[i - 15], w2 = w[i - 2];
        var a = hash[0], e = hash[4];
        var temp1 = hash[7]
          + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25))
          + ((e & hash[5]) ^ ((~e) & hash[6]))
          + k[i]
          + (w[i] = (i < 16) ? w[i] : (
            w[i - 16]
            + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3))
            + w[i - 7]
            + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10))
          ) | 0);
        var temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22))
          + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2]));
        hash = [(temp1 + temp2) | 0].concat(hash);
        hash[4] = (hash[4] + temp1) | 0;
      }
      for (i = 0; i < 8; i++) { hash[i] = (hash[i] + oldHash[i]) | 0; }
    }
    for (i = 0; i < 8; i++) {
      for (j = 3; j + 1; j--) {
        var b = (hash[i] >> (j * 8)) & 255;
        result += ((b < 16) ? 0 : '') + b.toString(16);
      }
    }
    return result;
  }

  function b64u(s) {
    try {
      return btoa(unescape(encodeURIComponent(s)))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
    } catch (e) { return ''; }
  }
  function unb64u(s) {
    try { return decodeURIComponent(escape(atob(s.replace(/-/g, '+').replace(/_/g, '/')))); }
    catch (e) { return ''; }
  }
  function rand(n) {
    var s = '', chars = '0123456789abcdef';
    try {
      var a = new Uint8Array(n);
      (window.crypto || window.msCrypto).getRandomValues(a);
      for (var i = 0; i < n; i++) s += chars[a[i] & 15];
    } catch (e) {
      for (var j = 0; j < n; j++) s += chars[Math.floor(Math.random() * 16)];
    }
    return s;
  }
  function resolveLang(lang) {
    if (lang && lang !== 'auto' && STR[lang]) return lang;
    var nav = ((navigator.languages && navigator.languages[0]) || navigator.language || 'en').slice(0, 2).toLowerCase();
    return STR[nav] ? nav : 'en';
  }
  function resolveTheme(theme) {
    if (theme === 'light' || theme === 'dark') return theme;
    try { return (window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light'; }
    catch (e) { return 'light'; }
  }
  function powBits(challenge) {
    try {
      var dot = challenge.lastIndexOf('.');
      var p = JSON.parse(unb64u(challenge.slice(0, dot)));
      var d = p && typeof p.d === 'number' ? p.d : 12;
      if (d <= 0 || d > 24) d = 12;
      return Math.round(d / 4) * 4;
    } catch (e) { return 12; }
  }
  function solvePow(challenge, bits) {
    var zeros = Math.max(1, Math.round(bits / 4));
    var prefix = new Array(zeros + 1).join('0');
    for (var p = 0; p < 2000000; p++) {
      var h = sha256(challenge + '.' + p);
      if (h && h.slice(0, zeros) === prefix) return { p: p, h: h };
    }
    return { p: p, h: '' };
  }

  var STYLE = '.vg-root{display:inline-block;width:100%;max-width:340px;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;direction:ltr}'
    + '.vg-box{border:1px solid #d4d4d8;border-radius:10px;background:#fff;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.07);cursor:pointer;transition:border-color .2s,box-shadow .2s;-webkit-user-select:none;user-select:none}'
    + '.vg-box:focus-visible{outline:2px solid #10b981;outline-offset:2px}'
    + '.vg-dark{background:#18181b;border-color:#3f3f46}'
    + '.vg-main{display:flex;align-items:center;gap:11px;padding:15px 16px;min-height:52px}'
    + '.vg-cb{width:24px;height:24px;border:2px solid #a1a1aa;border-radius:5px;background:#fff;flex:none;display:flex;align-items:center;justify-content:center;position:relative;transition:border-color .15s,background .15s}'
    + '.vg-dark .vg-cb{background:transparent;border-color:#52525b}'
    + '.vg-box:hover .vg-cb{border-color:#10b981}'
    + '.vg-cb svg{width:15px;height:15px;opacity:0;transform:scale(.3);transition:opacity .18s ease,transform .18s ease}'
    + '.vg-ok .vg-cb{background:#10b981;border-color:#10b981}'
    + '.vg-ok .vg-cb svg{opacity:1;transform:scale(1)}'
    + '.vg-label{font-size:14px;color:#3f3f46;font-weight:500;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}'
    + '.vg-dark .vg-label{color:#e4e4e7}'
    + '.vg-ok .vg-label{color:#059669;font-weight:600}'
    + '.vg-err .vg-label{color:#dc2626;font-weight:500}'
    + '.vg-brand{display:flex;align-items:center;gap:5px;color:#71717a;font-size:11.5px;font-weight:700;letter-spacing:.01em;flex:none}'
    + '.vg-brand svg{width:18px;height:18px;display:block}'
    + '.vg-foot{display:flex;justify-content:flex-end;align-items:center;gap:5px;padding:4px 12px 6px;border-top:1px solid #f4f4f5;background:#fafafa}'
    + '.vg-dark .vg-foot{border-top-color:#27272a;background:#131316}'
    + '.vg-foot svg{width:10px;height:10px;display:block;color:#a1a1aa}'
    + '.vg-foot a{font-size:10.5px;color:#a1a1aa;text-decoration:none;font-weight:500}'
    + '.vg-foot a:hover{color:#10b981;text-decoration:underline}'
    + '.vg-spinner{position:absolute;inset:2px;border-radius:50%;border:2px solid #e4e4e7;border-top-color:#10b981;opacity:0;transition:opacity .15s;animation:vg-spin .8s linear infinite}'
    + '.vg-dark .vg-spinner{border-color:#3f3f46}'
    + '.vg-checking .vg-spinner{opacity:1}'
    + '.vg-ok{border-color:#10b981 !important;box-shadow:0 0 0 3px rgba(16,185,129,.14)}'
    + '.vg-err{border-color:#f87171 !important}'
    + '@keyframes vg-spin{to{transform:rotate(360deg)}}'
    + '@media (prefers-reduced-motion:reduce){.vg-spinner{animation-duration:2s}}';

  var LOGO = '<svg viewBox="0 0 24 24" fill="none"><path d="M12 2.5 20 5.5v6c0 4.8-3.3 9-8 10.7C7.3 20.5 4 16.3 4 11.5v-6L12 2.5Z" fill="#10b981" fill-opacity="0.14" stroke="#10b981" stroke-width="1.5" stroke-linejoin="round"/><path d="m8.6 12 2.4 2.4 4.4-4.8" stroke="#10b981" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>';
  var CHECK = '<svg viewBox="0 0 24 24" fill="none"><path d="m5 13 4.2 4.2L19 7" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>';
  var MINI = '<svg viewBox="0 0 24 24" fill="none"><path d="M12 2.5 20 5.5v6c0 4.8-3.3 9-8 10.7C7.3 20.5 4 16.3 4 11.5v-6L12 2.5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>';

  function ensureStyle() {
    if (document.getElementById('vg-style')) return;
    var st = document.createElement('style');
    st.id = 'vg-style';
    st.textContent = STYLE;
    document.head.appendChild(st);
  }

  function render(target, opts) {
    opts = opts || {};
    var el = typeof target === 'string' ? document.querySelector(target) : target;
    if (!el || !el.appendChild) return null;
    ensureStyle();
    var lang = resolveLang(opts.lang);
    var t = STR[lang];
    var theme = resolveTheme(opts.theme);
    var sitekey = opts.sitekey || '';
    var inputName = opts.inputName || 'veilguard-token';
    var mode = opts.mode === 'static' ? 'static' : 'server';
    var challengeUrl = opts.challengeUrl || ('/veilguard-challenge?sitekey=' + encodeURIComponent(sitekey));

    var st = { mm: 0, ds: 0, kd: 0, td: 0, pd: 0, scc: 0, load: Date.now(), token: '', done: false, busy: false };
    var lastXY = null;

    function track(evt, fn) { document.addEventListener(evt, fn, { passive: true, capture: true }); }
    track('mousemove', function (e) {
      st.mm++;
      if (lastXY) { var dx = e.clientX - lastXY[0], dy = e.clientY - lastXY[1]; st.ds += Math.sqrt(dx * dx + dy * dy); }
      lastXY = [e.clientX, e.clientY];
    });
    track('keydown', function () { st.kd++; });
    track('touchstart', function () { st.td++; });
    track('pointerdown', function () { st.pd++; });
    track('scroll', function () { st.scc++; });

    el.innerHTML = '';
    el.className += ' vg-root';
    var box = document.createElement('div');
    box.className = 'vg-box' + (theme === 'dark' ? ' vg-dark' : '');
    box.setAttribute('role', 'checkbox');
    box.setAttribute('aria-checked', 'false');
    box.setAttribute('aria-label', t.nb);
    box.tabIndex = 0;
    box.innerHTML = '<div class="vg-main">'
      + '<div class="vg-cb"><span class="vg-spinner"></span>' + CHECK + '</div>'
      + '<span class="vg-label" aria-live="polite">' + t.nb + '</span>'
      + '<div class="vg-brand">' + LOGO + '<span>VeilGuard</span></div>'
      + '</div>'
      + '<div class="vg-foot">' + MINI + '<a href="' + LINK + '" target="_blank" rel="noopener noreferrer">veilguard.online</a></div>';
    var input = document.createElement('input');
    input.type = 'hidden';
    input.name = inputName;
    el.appendChild(box);
    el.appendChild(input);

    var label = box.querySelector('.vg-label');

    function setState(s) {
      var cls = 'vg-box' + (theme === 'dark' ? ' vg-dark' : '');
      if (s === 'checking') cls += ' vg-checking';
      if (s === 'ok') cls += ' vg-ok';
      if (s === 'err') cls += ' vg-err';
      box.className = cls;
      box.setAttribute('aria-checked', s === 'ok' ? 'true' : 'false');
      label.textContent = s === 'checking' ? t.ck : s === 'ok' ? t.vd : s === 'err' ? t.er : t.nb;
    }

    function detect() {
      var flags = 0;
      try { if (navigator.webdriver === true || !!window.webdriver) flags |= 1; } catch (e) {}
      if (BOT_UA.test(navigator.userAgent)) flags |= 2;
      try {
        if (window._phantom || window.callPhantom || window.__nightmare || window.domAutomation || window.__selenium_unwrapped) flags |= 4;
      } catch (e) {}
      try { if ((window.outerWidth === 0 || window.outerHeight === 0) && window.innerWidth > 0) flags |= 8; } catch (e) {}
      try {
        var c = document.createElement('canvas');
        var gl = c.getContext && (c.getContext('webgl') || c.getContext('experimental-webgl'));
        if (gl) {
          var dbg = gl.getExtension('WEBGL_debug_renderer_info');
          var r = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : '';
          if (typeof r === 'string' && /swiftshader|llvmpipe|software|basic render/i.test(r)) flags |= 16;
        }
      } catch (e) {}
      try { if (!navigator.languages || !navigator.languages.length) flags |= 32; } catch (e) {}
      var isChromeUA = navigator.userAgent.indexOf('Chrome/') !== -1 && !/Edg|OPR|SamsungBrowser/i.test(navigator.userAgent);
      if (isChromeUA && !window.chrome) flags |= 64;
      if (isChromeUA && (!navigator.plugins || navigator.plugins.length === 0)) flags |= 128;

      var score = 100;
      if (flags & 31) { score = 0; }
      else {
        if (flags & 32) score -= 20;
        if (flags & 64) score -= 15;
        if (flags & 128) score -= 10;
        var dt = Date.now() - st.load;
        if (dt < 350) score -= 30;
        if (st.mm === 0 && st.td === 0 && st.kd === 0 && st.pd <= 1) score -= 25;
        else if (st.mm > 0 && st.ds < 40) score -= 10;
        if (score < 0) score = 0;
        if (score > 100) score = 100;
      }
      return { flags: flags, score: score };
    }

    function staticChallenge() {
      var now = Date.now();
      return b64u(JSON.stringify({ n: rand(16), i: now, e: now + 300000, k: sitekey, d: 12 }));
    }

    function activate() {
      if (st.done || st.busy) return;
      st.busy = true;
      setState('checking');
      var started = Date.now();
      var finish = function (challenge) {
        if (!challenge) { setState('err'); st.busy = false; return; }
        try {
          var det = detect();
          var pow = solvePow(challenge, powBits(challenge));
          var att = {
            v: 1, ch: challenge, ts: Date.now(), sc: det.score, hl: det.flags,
            lg: lang, ua: navigator.userAgent.slice(0, 180),
            ev: { mm: st.mm, ds: Math.round(st.ds), kd: st.kd, td: st.td, pd: st.pd, sc: st.scc, dt: started - st.load },
            pw: pow
          };
          st.token = 'v1.' + b64u(JSON.stringify(att));
          input.value = st.token;
        } catch (e) { st.token = ''; }
        var wait = Math.max(0, 550 + Math.random() * 350 - (Date.now() - started));
        setTimeout(function () {
          st.busy = false;
          if (st.token) {
            st.done = true;
            setState('ok');
            if (typeof opts.callback === 'function') { try { opts.callback(st.token); } catch (e2) {} }
          } else { setState('err'); }
        }, wait);
      };
      if (mode === 'static') { finish(staticChallenge()); return; }
      try {
        fetch(challengeUrl, { credentials: 'omit' })
          .then(function (r) { if (!r.ok) throw new Error('http ' + r.status); return r.json(); })
          .then(function (d) { finish(d && d.challenge ? String(d.challenge) : ''); })
          .catch(function () { setState('err'); st.busy = false; });
      } catch (e) { setState('err'); st.busy = false; }
    }

    box.addEventListener('click', activate);
    box.addEventListener('keydown', function (e) {
      if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') { e.preventDefault(); activate(); }
    });

    return {
      reset: function () { st.done = false; st.busy = false; st.token = ''; input.value = ''; setState('idle'); },
      getResponse: function () { return st.token; },
      getSiteKey: function () { return sitekey; }
    };
  }

  function auto() {
    var nodes = document.querySelectorAll('.veilguard[data-sitekey]');
    for (var i = 0; i < nodes.length; i++) {
      var n = nodes[i];
      if (n.getAttribute('data-vg-init') === '1') continue;
      n.setAttribute('data-vg-init', '1');
      render(n, {
        sitekey: n.getAttribute('data-sitekey') || '',
        lang: n.getAttribute('data-lang') || 'auto',
        theme: n.getAttribute('data-theme') || 'auto',
        mode: n.getAttribute('data-mode') || 'server',
        challengeUrl: n.getAttribute('data-challenge-url') || undefined,
        inputName: n.getAttribute('data-input-name') || 'veilguard-token'
      });
    }
  }

  if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', auto); }
  else { auto(); }

  window.VeilGuard = { render: render, auto: auto, version: '1.0.0' };
})();
form.html
<!-- VeilGuard widget — place inside your <form> -->
<div class="veilguard" data-sitekey="vg_YOUR_SITE_KEY" data-challenge-url="veilguard-challenge.php"></div>

<!-- Load the widget once per page (before </body>) -->
<script src="veilguard-widget.js"></script>

2 · Проверка: выполняется на вашем сервере

veilguard.php
<?php
/**
 * VeilGuard v1.0.0 — self-hosted verification library for PHP
 * Docs: https://veilguard.online
 */

define('VG_MIN_SCORE', 55);
define('VG_TTL', 300);        // seconds — challenge lifetime
define('VG_GRACE', 60);       // seconds — grace after expiry
define('VG_MIN_SOLVE_MS', 250);

function vg_b64u_decode(string $s): string {
    return base64_decode(strtr($s, '-_', '+/'));
}

/** Issue a signed single-use challenge (used by your challenge endpoint). */
function vg_issue_challenge(string $site_key, string $secret): string {
    $now = (int) (microtime(true) * 1000);
    $payload = json_encode(array(
        'n' => bin2hex(random_bytes(8)),
        'i' => $now,
        'e' => $now + VG_TTL * 1000,
        'k' => $site_key,
        'd' => 12,
    ));
    $body = rtrim(strtr(base64_encode($payload), '+/', '-_'), '=');
    return $body . '.' . hash_hmac('sha256', $body, $secret);
}

/**
 * Verify a token submitted by the visitor.
 * Returns array('success' => bool, 'score' => int, 'reason' => string)
 */
function vg_verify_token(string $token, string $secret, string $site_key, int $min_score = VG_MIN_SCORE): array {
    $fail = function (string $reason): array { return array('success' => false, 'reason' => $reason); };

    if (strlen($token) > 4096 || strpos($token, 'v1.') !== 0) {
        return $fail('Malformed token');
    }
    $json = json_decode(vg_b64u_decode(substr($token, 3)), true);
    if (!is_array($json) || !isset($json['ch'])) {
        return $fail('Malformed payload');
    }

    // 1. Cryptographic challenge validation (HMAC-SHA256)
    $ch = $json['ch'];
    $dot = strrpos($ch, '.');
    if ($dot === false) { return $fail('Malformed challenge'); }
    $body = substr($ch, 0, $dot);
    $sig  = substr($ch, $dot + 1);
    if (!hash_equals(hash_hmac('sha256', $body, $secret), $sig)) {
        return $fail('Invalid challenge signature');
    }
    $p = json_decode(vg_b64u_decode($body), true);
    if (!is_array($p)) { return $fail('Malformed challenge payload'); }
    if (($p['k'] ?? '') !== $site_key) { return $fail('Site key mismatch'); }

    // 2. Timing window — server clock only
    $now = (int) (microtime(true) * 1000);
    if ($now > $p['e'] + VG_GRACE * 1000) { return $fail('Challenge expired'); }
    if ($now < $p['i']) { return $fail('Challenge issued in the future'); }
    if ($now - $p['i'] < VG_MIN_SOLVE_MS) { return $fail('Solved impossibly fast'); }

    // 3. Proof-of-work bound to this challenge
    $pow = $json['pw'] ?? null;
    if (!is_array($pow) || !isset($pow['p'])) { return $fail('Missing proof of work'); }
    $hash = hash('sha256', $ch . '.' . $pow['p']);
    $zeros = (int) ceil($p['d'] / 4);
    if (substr($hash, 0, $zeros) !== str_repeat('0', $zeros)) {
        return $fail('Invalid proof of work');
    }

    // 4. Automation fingerprints reported by the widget (bitmask)
    $hl = (int) ($json['hl'] ?? 255);
    if ($hl & 1)  { return $fail('Automation framework detected'); }
    if ($hl & 2)  { return $fail('Known bot user agent'); }
    if ($hl & 4)  { return $fail('Automation framework detected'); }
    if ($hl & 8)  { return $fail('Headless browser detected'); }
    if ($hl & 16) { return $fail('Software renderer detected'); }

    // 5. Behavioral score threshold
    $score = (int) ($json['sc'] ?? 0);
    if ($score < $min_score) { return $fail('Score below threshold'); }

    // 6. Single-use nonce — replay protection
    if (!vg_nonce_fresh($p['n'], $p['e'])) { return $fail('Token replay detected'); }

    return array('success' => true, 'score' => $score);
}

/** File-based single-use nonce store (shared-hosting friendly). */
function vg_nonce_fresh(string $nonce, int $expires_ms): bool {
    $file = sys_get_temp_dir() . '/veilguard-nonces.json';
    $used = array();
    if (is_file($file)) {
        $used = json_decode((string) @file_get_contents($file), true) ?: array();
    }
    $now = (int) (microtime(true) * 1000);
    foreach ($used as $k => $exp) {
        if ((int) $exp < $now) { unset($used[$k]); }
    }
    if (isset($used[$nonce])) { return false; }
    $used[$nonce] = $expires_ms + 60000;
    @file_put_contents($file, json_encode($used), LOCK_EX);
    return true;
}
veilguard-challenge.php
<?php
/**
 * VeilGuard challenge endpoint — serves single-use challenges to the widget.
 * File: veilguard-challenge.php
 */
require __DIR__ . '/veilguard.php';

$siteKey = 'vg_YOUR_SITE_KEY';   // public
$secret  = 'vgs_YOUR_SECRET_KEY'; // private — never expose in HTML

header('Content-Type: application/json');
header('Cache-Control: no-store');
echo json_encode(array('challenge' => vg_issue_challenge($siteKey, $secret)));
signup.php
<?php
/**
 * VeilGuard example — a bot-proof signup form.
 * File: signup.php
 */
require __DIR__ . '/veilguard.php';

$siteKey = 'vg_YOUR_SITE_KEY';
$secret  = 'vgs_YOUR_SECRET_KEY';
$error = null;
$success = null;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['veilguard-token'] ?? '';
    $result = vg_verify_token($token, $secret, $siteKey);
    if (!$result['success']) {
        http_response_code(403);
        $error = 'Bot detected: ' . $result['reason'];
    } else {
        // Human confirmed — continue with your logic here.
        $success = 'Welcome, human! Trust score: ' . $result['score'];
    }
}
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Signup — protected by VeilGuard</title>
</head>
<body>
  <?php if ($success): ?><p style="color:green"><?= htmlspecialchars($success) ?></p><?php endif; ?>
  <?php if ($error): ?><p style="color:red"><?= htmlspecialchars($error) ?></p><?php endif; ?>

  <form method="post">
    <input type="email" name="email" placeholder="Email" required>

    <!-- VeilGuard widget -->
    <div class="veilguard" data-sitekey="<?= htmlspecialchars($siteKey) ?>"
         data-challenge-url="veilguard-challenge.php"></div>

    <button type="submit">Sign up</button>
  </form>

  <script src="veilguard-widget.js"></script>
</body>
</html>

Ключами также можно управлять в админ-панели.

Своя собственная статистика

VeilGuard ничего не отправляет домой. Если вы хотите знать, как часто используется ваша капча, логируйте результаты сами - два маленьких фрагмента, один JSON-файл, без базы данных.

Два фрагмента
Без фреймворка и базы данных - чистый PHP, работающий на любом хостинге. Та же схема работает в Node и Python.

1 · В обработчике формы

form-handler.php
<?php
// form-handler.php — your existing form handler.
require __DIR__ . '/veilguard.php';

// Your key pair from the integration section.
$siteKey = 'vg_YOUR_SITE_KEY';
$secret  = 'vgs_YOUR_SECRET_KEY';

$result = vg_verify_token($_POST['veilguard-token'] ?? '', $secret, $siteKey);

// --- Statistics: one row per attempt, stored only on your server. ---
$file = __DIR__ . '/data/stats.json';
$rows = is_file($file) ? (json_decode((string) file_get_contents($file), true) ?: []) : [];
$rows[] = array(
    't' => gmdate('c'),                           // when
    'v' => $result['success'] ? 'human' : 'bot',  // verdict
    's' => (int) ($result['score'] ?? 0),         // score
);
@mkdir(dirname($file), 0755, true);
file_put_contents($file, json_encode($rows), LOCK_EX);

if (!$result['success']) {
    http_response_code(403);
    exit($result['reason'] ?? 'Bot detected');
}

// Human confirmed — continue with your logic here.

Как это может выглядеть

VeilGuard
Статистика на собственном сервере
демо-данные

1248

Проверки

1173

Люди

75

Боты

14

Сегодня

Люди Боты

Демонстрационные данные - ваша карточка покажет ваши реальные числа.

Файл статистики остаётся на вашем сервере - как и всё в VeilGuard, ничего никуда не отправляется. Закройте веб-доступ к data/ (например, через .htaccess), как описано в ките.

Тарифы

VeilGuard бесплатен, включая коммерческое использование. Платных тарифов нет.

Получить ключи
$0 навсегда
  • Неограниченные проверки
  • Неограниченное число сайтов
  • Все 9 языков
  • Проверка на своём сервере
  • Без трекинга и cookies
  • Коммерческое использование разрешено

Частые вопросы

Напишите нам

Вопросы по интеграции, отзывы или бот, который всё же прорвался? Напишите нам - эта форма доказывает принципы VeilGuard на практике: она защищена собственной капчей.

  • Обычно отвечаем в течение 48 часов
  • Ваше сообщение хранится только на этом сервере
  • Без слежки, без рассылок
Отправить сообщение
Защищено VeilGuard - боты не доходят до папки «Входящие».