Bot Shield

Invisible proof-of-work bot protection. No CAPTCHAs, no image puzzles, no user friction. Two lines to integrate.

How it works

Every workspace can hand out short-lived challenge tokens. A challenge is a random prefix plus a target difficulty in bits. The client must find a nonce such that sha256(prefix + nonce) has N leading zero bits (default 18 ≈ ~200ms of CPU). Real browsers do this invisibly in a Web Worker. Scripted bots without JS can't solve it; those that can pay real CPU per request — which is what kills bulk abuse.

The proof (botToken) is an HMAC-signed capability bound to your workspace and an expiry. It lives 15 minutes in sessionStorage. Attach it to /v1/evaluate. Done.

Modes

Configure in Settings → Bot Protection.

  • off — ignored.
  • monitor — a missing/invalid proof adds a no_bot_proof signal to the risk score. Recommended for a week to see impact.
  • enforce — a missing proof forces STEP_UP_AUTH and the response returns a fresh challenge so the client can solve and retry.

Option A — drop-in widget (recommended)

html
<script
  src="https://veriloop.lovable.app/veriloop-bot.js"
  data-key="vl_live_..."></script>

<!-- Any form gets auto-protected: -->
<form data-veriloop-protect action="/signup" method="post">
  <input name="email" />
  <button>Sign up</button>
</form>

The widget warms a token in the background on page load. When the form submits, the widget injects a hidden veriloop_bot_token field. On your server, forward it to /v1/evaluate as botToken.

Or call the widget directly:

js
// Explicit API — auto-refreshes on 401
const res = await window.Veriloop.evaluate({
  action: "login",
  userId: "u_123",
  ip,
});
if (res.decision === "BLOCK") location.replace("/blocked");

Option B — manual PoW

js
// 1. Issue
const { challenge } = await fetch("/api/public/v1/challenge/issue", {
  method: "POST",
  headers: { authorization: `Bearer ${KEY}` },
}).then(r => r.json());

// 2. Solve
async function sha256Hex(s) { /* ...crypto.subtle.digest... */ }
function bits(hex) { /* count leading zero bits */ }
let nonce = 0, hex;
do { hex = await sha256Hex(challenge.prefix + (++nonce)); }
while (bits(hex) < challenge.difficulty);

// 3. Verify
const { botToken } = await fetch("/api/public/v1/challenge/verify", {
  method: "POST",
  headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
  body: JSON.stringify({ challenge, nonce: String(nonce) }),
}).then(r => r.json());

// 4. Attach to evaluate calls
await fetch("/api/public/v1/evaluate", {
  method: "POST",
  headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
  body: JSON.stringify({ action: "login", userId, botToken }),
});

What if enforce mode returns a challenge?

In enforce mode, a request without a valid botToken returns decision: STEP_UP_AUTH and a fresh botProtection.challenge. Solve it, retry with the new token, and continue.

Rate limits

Public endpoints are rate-limited per (API key + client IP) at roughly 60 rps burst / 600 rpm sustained for evaluate, and 20 rps for challenge endpoints. Exceeding returns 429 with Retry-After.

Testing

bash
VERILOOP_KEY=vl_live_... VERILOOP_BASE=https://YOUR-APP.lovable.app \
  node scripts/test-bot-shield.mjs

Signals it feeds

no_bot_proof, headless_ua, and bot_burst are all learned weights. Label events in the dashboard to teach the engine what's real bot traffic vs benign automation.

Bot Shield is stateless. Nothing about your users is persisted for challenges — no cookies, no tracking.