Un captcha que detiene a los bots,
no a los humanos.

VeilGuard verifica a los visitantes con señales invisibles. Sin puzzles de imágenes, sin llamadas a terceros. Todo se ejecuta en tu propio servidor.

Sin registro, sin tarjeta de crédito. La configuración tarda unos dos minutos.

example.com/signup

Crea tu cuenta

Verified!
VeilGuard

Pruébalo por tu cuenta

El widget de abajo es real: el desafío, la prueba de trabajo, el análisis de señales y la validación en el servidor se ejecutan en esta página.

Formulario de registro de ejemplo
example.com/signup — veilguard-token

Envía un token falsificado con huellas de automatización.

Veredicto del servidor
POST verify.php — Los tokens se validan localmente con HMAC-SHA256. Los humanos pasan, los bots no.
Envía el formulario para ver qué decidiría tu servidor.
Qué comprueba VeilGuard
  • Frameworks de automatización (WebDriver, Puppeteer, Playwright)
  • Navegadores headless y renderizadores por software
  • User agents de bots y crawlers conocidos
  • Entropía del comportamiento: señales de ratón, teclado y táctil
  • Prueba de trabajo ligada a un desafío de un solo uso
  • Análisis de tiempos y repetición en tu servidor

Qué hace VeilGuard

Seis cosas que hace bien.

Tasa de bloqueo cercana al 100 %

Un motor multiseñal combina huellas de automatización, entropía del comportamiento, prueba de trabajo y análisis de tiempos en el servidor.

También detiene a los bots de IA

Chrome headless, la automatización por CDP y los agentes con script se identifican por su huella y se bloquean, no solo los crawlers ingenuos.

Integración en 2 minutos

Un fragmento para el widget y una llamada en tu servidor. Copia y pega para PHP, Node.js, Python y sitios estáticos.

Funciona 100 % en tu servidor

La verificación es una comprobación HMAC-SHA256 local. Aunque veilguard.online se caiga, tu protección sigue en pie.

Invisible por diseño

Ni semáforos ni hidrantes. Los humanos hacen un clic y ven «¡Verificado!».

Privacidad ante todo y compatible con el RGPD

Sin cookies, sin píxeles de rastreo, sin solicitudes a terceros. Las señales efímeras viven solo dentro del token.

Cómo funciona

Tres pasos, sin necesidad de cuenta.

  1. 1.

    Genera claves gratis

    Sin registro, sin correo. Al instante recibes una clave de sitio y una clave secreta.

  2. 2.

    Pega dos fragmentos

    El script del widget va en tu formulario y la llamada de verificación en tu backend.

  3. 3.

    Listo

    Los tokens se validan localmente con HMAC-SHA256. Los humanos pasan, los bots no.

Integración en 2 minutos

Genera tus claves gratis, elige tu stack y pega el código. Nada sale de tu servidor.

1. Genera claves gratis
Sin registro, sin correo. Al instante recibes una clave de sitio y una clave secreta.
2. Pega dos fragmentos
El script del widget va en tu formulario y la llamada de verificación en tu backend.

1 · Widget: pégalo en tu formulario

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 · Verificación: se ejecuta en tu 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>

Las claves también se pueden gestionar en el panel de administración.

Tus propias estadísticas

VeilGuard nunca envía datos a casa. Si quieres saber cuánto se usa tu captcha, registra tú mismo los resultados: dos fragmentos pequeños, un archivo JSON, sin base de datos.

Dos fragmentos
Sin framework ni base de datos: PHP puro que funciona en cualquier hosting. El mismo patrón sirve para Node y Python.

1 · En tu gestor de formulario

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.

Así podría verse

VeilGuard
Estadísticas autoalojadas
datos de ejemplo

1248

Verificaciones

1173

Humanos

75

Bots

14

Hoy

Humanos Bots

Datos de ejemplo - tu tarjeta muestra tus números reales.

El archivo de estadísticas queda en tu servidor - como todo en VeilGuard, no se envía nada. Bloquea el acceso web a data/ (p. ej. con .htaccess), como se describe en el kit.

Precios

VeilGuard es gratis, también para uso comercial. No hay planes de pago.

Obtén tus claves
$0 para siempre
  • Verificaciones ilimitadas
  • Sitios ilimitados
  • Los 9 idiomas
  • Verificación autoalojada
  • Sin rastreo ni cookies
  • Uso comercial permitido

FAQ

Escríbenos

¿Preguntas sobre la integración, sugerencias o un bot que se coló? Escríbenos: este formulario practica lo que VeilGuard predica, está protegido por su propio captcha.

  • Respondemos normalmente en 48 horas
  • Tu mensaje se guarda únicamente en este servidor
  • Sin rastreo, sin boletines
Enviar un mensaje
Protegido por VeilGuard: los bots nunca llegan a la bandeja.