Integrations · Cloudflare

Cloudflare Worker Integration

Run ZeroBot at Cloudflare's edge — every request is screened before it reaches your origin server. ~80ms first-touch, sub-10ms cached, fail-open by default.

How it works

One Cloudflare Worker sits between visitors and your origin. For every request, the Worker calls /v3/openapi, gets a verdict, and either lets the request through to your server or blocks it with a 403.

Visitor → Cloudflare Edge → [ZeroBot Worker] ──┐ ↓ https://zerobot.info/v3/openapi ↓ verdict: allow / block / score ↓ ┌──── allow ────► origin └──── block ────► 403
Edge-cached — KV cache per IP Fail-open — never breaks your site Static-asset bypass — no API calls for CSS/images Same scoring engine — as the WordPress plugin

Before you start

Deploy via the Cloudflare dashboard (no CLI)

1
Create a new Worker

Cloudflare dashboard → Workers & Pages → Create → Create Worker. Name it zerobot-edge and click Deploy.

2
Paste the Worker source

Click Edit code, replace the default content with the source below, then click Deploy again.

3
Set your license key

In the Worker, go to Settings → Variables and Secrets, click Add, type ZEROBOT_LICENSE, paste your key, choose Encrypt, save.

4
Bind the Worker to your domain

Settings → Domains & Routes → Add → Route. Use a pattern like yourdomain.com/* and select your zone. Save.

5
(Optional) Enable edge caching

Workers & Pages → KV → Create namespace named ZEROBOT_KV. Back in your Worker → Settings → Bindings → Add → KV namespace → variable name ZEROBOT_KV → select the namespace. Verdicts are now cached at the edge for 5 minutes per IP.

Done. Visit your site. Open DevTools → Network — you'll see X-ZeroBot-Verdict and X-ZeroBot-Score headers on every page. Bots get a 403 block page instead.

Worker source code

Copy this into the Cloudflare Worker editor.

/**
 * ZeroBot Edge Worker — required env: ZEROBOT_LICENSE (Secret).
 * Optional: ZEROBOT_KV, ZEROBOT_CACHE_TTL, ZEROBOT_FAIL_MODE.
 */
const ZEROBOT_API = 'https://zerobot.info/v3/openapi';
const STATIC_RE  = /\.(jpe?g|png|gif|webp|avif|svg|ico|css|js|mjs|woff2?|ttf|eot|otf|map|mp4|webm|ogg|mp3|wav|pdf|zip)$/i;

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (STATIC_RE.test(url.pathname)) return fetch(request);
    if (!env.ZEROBOT_LICENSE) return fetch(request);

    const ip       = request.headers.get('CF-Connecting-IP') || '0.0.0.0';
    const ua       = request.headers.get('User-Agent') || '';
    const domain   = url.hostname;
    const failMode = (env.ZEROBOT_FAIL_MODE || 'open').toLowerCase();
    const cacheKey = `zb:${domain}:${ip}`;
    let verdict  = null;

    if (env.ZEROBOT_KV) {
      try { verdict = await env.ZEROBOT_KV.get(cacheKey, 'json'); } catch (_) {}
    }

    if (!verdict) {
      try {
        const apiUrl = new URL(ZEROBOT_API);
        apiUrl.searchParams.set('license',   env.ZEROBOT_LICENSE);
        apiUrl.searchParams.set('ip',        ip);
        apiUrl.searchParams.set('domain',    domain);
        apiUrl.searchParams.set('useragent', ua);

        const apiResp = await fetch(apiUrl.toString(), { cf: { cacheTtl: 60, cacheEverything: true } });
        if (apiResp.ok) {
          verdict = await apiResp.json();
          if (env.ZEROBOT_KV && verdict) {
            const ttl = parseInt(env.ZEROBOT_CACHE_TTL || '300', 10);
            ctx.waitUntil(env.ZEROBOT_KV.put(cacheKey, JSON.stringify(verdict), { expirationTtl: ttl }));
          }
        } else if (failMode === 'closed') {
          return blockResponse({ reason: 'ZeroBot API error' });
        }
      } catch (e) {
        if (failMode === 'closed') return blockResponse({ reason: 'API unreachable' });
        return fetch(request);
      }
    }

    if (verdict?.is_bot) return blockResponse(verdict);

    const originResp = await fetch(request);
    const headers    = new Headers(originResp.headers);
    if (verdict) {
      headers.set('X-ZeroBot-Verdict', String(verdict.reason || 'clean'));
      headers.set('X-ZeroBot-Score',   String(verdict.risk_score || 0));
    }
    return new Response(originResp.body, { status: originResp.status, statusText: originResp.statusText, headers });
  }
};

function blockResponse(v) {
  return new Response(blockHtml(v), {
    status: 403,
    headers: {
      'Content-Type':        'text/html; charset=utf-8',
      'Cache-Control':       'no-store',
      'X-ZeroBot-Verdict':   String(v.reason || 'bot'),
      'X-ZeroBot-Score':     String(v.risk_score || 0)
    }
  });
}

function blockHtml(v) { /* … minimal HTML block page, see full source */ }

Full source (with the styled block page) is at /cloudflare-worker/worker.js. wrangler.toml: /cloudflare-worker/wrangler.toml.

Deploy via wrangler CLI (recommended for teams)

npm install -g wrangler

mkdir zerobot-edge && cd zerobot-edge
curl -O https://zerobot.info/cloudflare-worker/worker.js
curl -O https://zerobot.info/cloudflare-worker/wrangler.toml

wrangler login
wrangler secret put ZEROBOT_LICENSE

# optional: edge cache
wrangler kv namespace create ZEROBOT_KV

wrangler deploy

Environment variables

Name Required Default Description
ZEROBOT_LICENSEYesYour license key. Set as a Secret (encrypted), not a plain variable.
ZEROBOT_KVNoKV namespace binding. Verdicts cached per IP for ZEROBOT_CACHE_TTL seconds.
ZEROBOT_CACHE_TTLNo300How long (in seconds) to cache a verdict for the same IP.
ZEROBOT_FAIL_MODENoopenopen — let traffic through if the API is unreachable. closed — block on API errors.

Troubleshooting