Um captcha que bloqueia os bots,
não os humanos.

O VeilGuard verifica os visitantes com sinais invisíveis. Sem quebra-cabeças de imagens, sem chamadas de terceiros. Tudo roda no seu próprio servidor.

Sem cadastro, sem cartão de crédito. A configuração leva cerca de dois minutos.

example.com/signup

Crie sua conta

Verified!
VeilGuard

Teste você mesmo

O widget abaixo é real: desafio, prova de trabalho, análise de sinais e validação no servidor, tudo rodando nesta página.

Exemplo de formulário de cadastro
example.com/signup — veilguard-token

Envia um token forjado com impressões digitais de automação.

Veredito do servidor
POST verify.php — Os tokens são validados localmente com HMAC-SHA256. Humanos passam, bots não.
Envie o formulário para ver o que o seu servidor decidiria.
O que o VeilGuard verifica
  • Frameworks de automação (WebDriver, Puppeteer, Playwright)
  • Navegadores headless e renderizadores de software
  • User agents de bots e crawlers conhecidos
  • Entropia comportamental: sinais de mouse, teclado e toque
  • Prova de trabalho vinculada a um desafio de uso único
  • Análise de tempo e repetição no seu servidor

O que o VeilGuard faz

Seis coisas que ele faz bem.

Taxa de bloqueio de quase 100 %

Um motor multi-sinal combina impressões digitais de automação, entropia comportamental, prova de trabalho e análise de tempo no servidor.

Também bloqueia os bots de IA

Chrome headless, automação via CDP e agentes controlados por script têm a impressão digital coletada e são bloqueados, não só crawlers ingênuos.

Integração em 2 minutos

Um trecho para o widget e uma chamada no seu servidor. Copie e cole para PHP, Node.js, Python e sites estáticos.

Roda 100 % no seu servidor

A verificação é uma checagem HMAC-SHA256 local. Mesmo que o veilguard.online caia, a sua proteção continua de pé.

Invisível por natureza

Sem semáforos, sem hidrantes. Humanos clicam uma vez e veem “Verificado!”.

Privacidade antes de tudo, compatível com o RGPD

Sem cookies, sem pixels de rastreamento, sem requisições de terceiros. Os sinais efêmeros vivem apenas dentro do token.

Como funciona

Três etapas, sem necessidade de conta.

  1. 1.

    Gere chaves grátis

    Sem cadastro, sem e-mail. Você recebe na hora uma chave de site e uma chave secreta.

  2. 2.

    Cole dois trechos de código

    O script do widget vai no seu formulário e a chamada de verificação no seu backend.

  3. 3.

    Pronto

    Os tokens são validados localmente com HMAC-SHA256. Humanos passam, bots não.

Integração em 2 minutos

Gere suas chaves grátis, escolha a sua stack e cole o código. Nada sai do seu servidor.

1. Gere chaves grátis
Sem cadastro, sem e-mail. Você recebe na hora uma chave de site e uma chave secreta.
2. Cole dois trechos de código
O script do widget vai no seu formulário e a chamada de verificação no seu backend.

1 · Widget: cole no seu formulário

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 · Verificação: roda no seu servidor

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>

As chaves também podem ser gerenciadas no painel de administração.

As suas próprias estatísticas

O VeilGuard nunca liga para casa. Se quiser saber com que frequência o seu captcha é usado, registe os resultados você mesmo - dois pequenos snippets, um ficheiro JSON, sem base de dados.

Dois snippets
Sem framework, sem base de dados - PHP puro que corre em qualquer alojamento. O mesmo padrão funciona em Node e Python.

1 · No seu processador de formulário

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.

Como poderia ficar

VeilGuard
Estatísticas autoalojadas
dados de exemplo

1248

Verificações

1173

Humanos

75

Bots

14

Hoje

Humanos Bots

Dados de exemplo - o seu cartão mostra os seus números reais.

O ficheiro de estatísticas fica no seu servidor - como tudo no VeilGuard, nada é enviado. Bloqueie o acesso web a data/ (p. ex. com .htaccess), como descrito no kit.

Preços

O VeilGuard é grátis, inclusive para uso comercial. Não há planos pagos.

Obtenha suas chaves
$0 para sempre
  • Verificações ilimitadas
  • Sites ilimitados
  • Os 9 idiomas
  • Verificação auto-hospedada
  • Sem rastreamento, sem cookies
  • Uso comercial permitido

FAQ

Fale conosco

Dúvidas sobre a integração, feedback ou um bot que passou despercebido? Escreva para nós - este formulário pratica o que o VeilGuard prega: é protegido pelo próprio captcha.

  • Normalmente respondemos em 48 horas
  • Sua mensagem fica armazenada apenas neste servidor
  • Sem rastreamento, sem newsletters
Enviar uma mensagem
Protegido pelo VeilGuard - bots nunca chegam à caixa de entrada.