Awesome Go

FCaptcha

CategorySecurity
SubcategorySecurity
Stars227

Self-hosted CAPTCHA server and Go client that detects bots, AI agents, and headless browsers through behavioral analysis and proof of work

About FCaptcha

This large README is shown as a plain-text preview. Read the full README at the source

<p align="center">
  <img src=".github/assets/hero.webp" alt="F***Captcha: a duck in a pink hoodie spray-painting the name on a wall" width="880">
</p>

# F***Captcha

**Open source CAPTCHA that blocks bots, vision AI agents, and automation - with a single click or less.**

![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Go](https://img.shields.io/badge/Go-1.26.10+-00ADD8?logo=go)
![Python](https://img.shields.io/badge/Python-3.12+-3776AB?logo=python)
![Node](https://img.shields.io/badge/Node-22+-339933?logo=node.js)
[![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?logo=docker)](https://github.com/WebDecoy/FCaptcha/pkgs/container/fcaptcha)
[![Go Reference](https://pkg.go.dev/badge/github.com/WebDecoy/FCaptcha.svg)](https://pkg.go.dev/github.com/WebDecoy/FCaptcha)
[![codecov](https://codecov.io/gh/WebDecoy/FCaptcha/graph/badge.svg)](https://app.codecov.io/gh/WebDecoy/FCaptcha)

**[Try the Live Demo](https://webdecoy.com/product/fcaptcha-demo/)**

[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/WebDecoy/FCaptcha)
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/template?referralCode=webdecoy&template=https://github.com/WebDecoy/FCaptcha)

FCaptcha is a modern CAPTCHA system designed to detect everything: traditional bots, headless browsers, automation frameworks, CAPTCHA farms, and the new generation of AI agents — from vision models that screenshot-and-click to computer-use agents that drive a real browser over the Chrome DevTools Protocol.

## Try it in 60 seconds

**1. Run the server**

```bash
export FCAPTCHA_SECRET="$(openssl rand -hex 32)"
docker run -d -p 127.0.0.1:3000:3000 -e FCAPTCHA_SECRET ghcr.io/webdecoy/fcaptcha:latest
```

The demo page is now at `http://localhost:3000/demo/`.

**2. Add the widget to a form**

```html
<form method="post" action="/signup">
  <div data-fcaptcha="your-site-key" data-endpoint="http://localhost:3000"></div>
  <button>Sign up</button>
</form>
<script src="http://localhost:3000/fcaptcha.js"></script>
```

Once the visitor passes, the token is posted with the form as `fcaptcha_token`.

**3. Verify the token on your backend** with the call you already make to Turnstile, reCAPTCHA or hCaptcha, pointed at your own server:

```diff
- https://challenges.cloudflare.com/turnstile/v0/siteverify
+ http://localhost:3000/turnstile/v0/siteverify
```

Production setup (Compose, Helm, Fly.io, building from source, native SDKs) is in the [full Quick Start](#quick-start).

## Why FCaptcha

- **It catches AI agents, not just bots.** Vision agents that screenshot and click, and computer-use agents that drive a real browser over CDP, leave input-event traces that FCaptcha scores. [How](#ai-agent-detection)
- **It replaces Turnstile, reCAPTCHA or hCaptcha by changing a base URL.** It serves the same `siteverify` contract on the same paths.
- **It runs on your server.** No cookies, no third parties, no phone-home. [COMPLIANCE.md](COMPLIANCE.md) lists every retention window.
- **No puzzle to fail.** Screen-reader, keyboard-only, tremor and elderly personas run against a false-positive budget in CI that fails the build when exceeded.
- **The algorithm is open.** Every score is itemised with a reason, and every threshold came from [measured browser traces](bench/).

## How it compares

| | FCaptcha | reCAPTCHA | hCaptcha | Turnstile | ALTCHA | mCaptcha |
|---|---|---|---|---|---|---|
| Self-hosted | Yes | No | No | No | Yes | Yes |
| Open source | MIT | No | No | No | MIT | AGPL |
| Image or audio puzzle | Never | Sometimes | Sometimes | Never | Never | Never |
| Visitor data sent to a third party | No | Google | hCaptcha | Cloudflare | No | No |
| Behavioural and environmental bot detection | Yes, open source | Yes, proprietary | Yes, proprietary | Yes, proprietary | Proof of work only[^altcha] | Proof of work only |
| Turnstile-compatible `siteverify` | Yes | No | No | Yes | No | No |

[^altcha]: The open-source ALTCHA widget. ALTCHA also sells separate commercial products.

## Runnable examples

- [Next.js App Router form](examples/nextjs/): checkbox widget, server-side
  verification, and replay/failure tests.
- [Hono middleware with FCaptcha verification](examples/hono/): browser demo,
  server-side token checks, and replay/failure tests.
- [Supabase Edge Function with FCaptcha verification](examples/supabase/):
  local browser demo, CORS handling, and single-use token validation.

- [FastAPI application with server-side FCaptcha verification](examples/fastapi/):
  invisible-mode browser form, an async verification dependency, and replay/failure tests.
- [Node.js contact form](examples/contact-form/): Japanese/English local demo.

## Integrations

- **Statamic:** the [Captcha addon](https://statamic.com/addons/aryeh-raber/captcha)
  supports FCaptcha from v1.18.0. Install it with
  `composer require aryehraber/statamic-captcha`, set `CAPTCHA_SERVICE=Fcaptcha`
  and `CAPTCHA_SERVER_URL` to your server, and add `{{ captcha }}` to your forms.
  [Setup guide](https://github.com/aryehraber/statamic-captcha#installation).
- **Gitea:** use its reCAPTCHA setting, pointed at your FCaptcha server:

  ```ini
  [service]
  ENABLE_CAPTCHA    = true
  CAPTCHA_TYPE      = recaptcha
  RECAPTCHA_URL     = https://your-fcaptcha-server.com/recaptcha/
  RECAPTCHA_SITEKEY = your-site-key
  RECAPTCHA_SECRET  = your FCAPTCHA_VERIFY_SECRET
  ```

### Apps that support Turnstile, hCaptcha or reCAPTCHA

If an app lets you change both the provider's script URL and its siteverify
URL, point them at your FCaptcha server. FCaptcha serves its widget on each
provider's script path, where it answers to that provider's JavaScript API
(`turnstile.render`, `hcaptcha.getResponse`, `grecaptcha.reset` and so on) and
fills that provider's form field:

| Provider | Script URL | Siteverify URL | Form field |
|---|---|---|---|
| Turnstile | `/turnstile/v0/api.js` | `/turnstile/v0/siteverify` | `cf-turnstile-response` |
| hCaptcha | `/1/api.js` | `/siteverify` | `h-captcha-response` and `g-recaptcha-response` |
| reCAPTCHA | `/recaptcha/api.js` | `/recaptcha/api/siteverify` | `g-recaptcha-response` |

`score` in the siteverify response is FCaptcha's, where lower is more human.
On `/recaptcha/api/siteverify` it follows reCAPTCHA v3 instead, where higher is
more human, so an app's existing threshold keeps working; FCaptcha's own score
is in `fcaptcha_score` there.

## All features

- **Drop-in for Turnstile / reCAPTCHA / hCaptcha** - Serves the same `siteverify` contract on the same paths, so migrating an existing backend is a base-URL change; tokens carry a signed `hostname` and `action` your app can check
- **Single click or invisible** - Checkbox mode like Turnstile/reCAPTCHA v2, or invisible mode like reCAPTCHA v3
- **AI agent detection** - Catches vision agents (screenshot→API→click), DOM/CDP-driven agents (Claude in Chrome, Operator-style computer use), and synthetic input that reports `isTrusted: true` — via input-event forensics and LLM think-time cadence
- **Declared & verified agents** - Flags self-declaring agents (ClaudeBot, GPTBot, ChatGPT-User, PerplexityBot, Bytespider…) and *cryptographically verifies* Web Bot Auth (RFC 9421) signed requests against the agent's published key directory, surfaced as a distinct category so your app can *allow* polite/verified agents and block the rest
- **Proof of Work** - Server-verified SHA-256 hashcash with 256-bit HMAC signing, per-challenge nonces, and signal commitment that binds the challenge to the collected signals. A liveness and timing gate rather than a cost function — see [what it does and does not buy you](#what-the-proof-of-work-does-and-does-not-buy-you)
- **Comprehensive bot detection** - Headless browsers, WebDriver, Puppeteer, Playwright, Selenium, plus CDP console-attach detection
- **Behavioral biometrics** - 40+ signals including micro-tremor, velocity/acceleration curves, trajectory analysis, coalesced pointer events, and teleport-click detection
- **Localized** - The widget ships 34 languages with automatic detection from `<html lang>` or the browser, right-to-left layout for Arabic/Hebrew/Persian, and a `strings` override for anything not bundled
- **Mobile-native** - Touch kinematics and passive device-sensor entropy, with accessibility exemptions for keyboard-only and touch users
- **TLS fingerprinting** - JA3 (client-supplied) and JA4 (un-spoofable, from a trusted reverse proxy) matched against known automation tools
- **Credential stuffing protection** - Form interaction analysis, timing, and programmatic submit/fill detection
- **Self-hosted & privacy-first** - No cookies, no third-party data sharing, no cross-site tracking. Nothing is written to disk and every retention window is enumerated in [COMPLIANCE.md](COMPLIANCE.md). Available on npm/CDN with Subresource Integrity when you'd rather not host the widget yourself
- **Accessible, and tested for it** - No visual or audio puzzle to fail. The widget's WCAG 2.2 AA position is verified in CI — contrast, focus visibility that survives a host stylesheet, target size, reflow, reduced motion — alongside a benchmark panel of screen-reader, keyboard-only, motor-tremor and elderly personas whose false-positive budget fails the build when exceeded
- **Input forensics** - Typing cadence and modality (keystrokes vs. paste vs. a value assigned into the DOM), scroll morphology, and platform contradictions like a Ctrl+V paste from a browser claiming macOS
- **Open algorithm** - Transparent, evidence-accumulating scoring across ~12 categories, fully auditable
- **Measured, not asserted** - A [benchmark harness](bench/) captures real browser traces for 14 human personas (keyboard-only, screen-reader, touch, tremor, elderly, DevTools-open, privacy-extension…) and reports a per-signal false-positive budget in CI. Every threshold in the behavioural layer was derived from it
- **Multi-language servers** - Go, Python, or Node.js, kept in lockstep

## Quick Start

### Docker (recommended)

A **prebuilt public image** is available at [`ghcr.io/webdecoy/fcaptcha`](https://github.com/WebDecoy/FCaptcha/pkgs/container/fcaptcha) for **Linux AMD64 and ARM64**. No GitHub login or source build is required. Use `:latest` to try the current image, or pin a release tag (for example, `:1.46.1`) or digest for reproducible deployments.

See the [Docker guide](docker/README.md) for local startup, Compose, version pinning, and upgrades.

Quick local start:

```bash
docker run -d --name fcaptcha -p 127.0.0.1:3000:3000 -e FCAPTCHA_SECRET="$(openssl rand -hex 32)" ghcr.io/webdecoy/fcaptcha:latest
```

This gives you:
- API at `http://localhost:3000/api/*`
- Client JS at `http://localhost:3000/fcaptcha.js`
- Demo page at `http://localhost:3000/demo/`

Docker Compose (single instance):

```bash
FCAPTCHA_SECRET="$(openssl rand -hex 32)" docker compose -f docker/docker-compose.yml up -d
```

FCaptcha state is process-local by default. Setting `REDIS_URL` shares PoW
challenges, token replay protection, Siteverify idempotency, rate limits,
suspicion, fingerprint cardinality, and site-key rotation guards across
replicas, so the Go, Node and Python servers can each run more than one.
Challenge and token consumption are atomic.

Both failure modes are closed: a server refuses to start if configured Redis is
unavailable, and fails closed rather than reverting to process-local state if it
becomes unavailable later.

Kubernetes:

```bash
helm install fcaptcha ./charts/fcaptcha \
  --namespace fcaptcha --create-namespace \
  --set secret=$(openssl rand -hex 32)

helm test fcaptcha -n fcaptcha
```

The chart refuses to install without a signing key — see
[charts/fcaptcha](charts/fcaptcha/README.md), which also covers the
`trustedProxies` setting that matters more behind an ingress controller than
anywhere else.

Deploy to Fly.io:

```bash
fly launch --copy-config
fly secrets set FCAPTCHA_SECRET="$(openssl rand -hex 32)"
```

Build from source:

```bash
docker build -f docker/Dockerfile -t fcaptcha .
docker run -d -p 3000:3000 -e FCAPTCHA_SECRET="$(openssl rand -hex 32)" fcaptcha
```

### Run from Source

Pick your language:

**Go (fastest)**
```bash
cd server-go
go build -o fcaptcha-server
FCAPTCHA_SECRET="$(openssl rand -hex 32)" ./fcaptcha-server
```

**Python (FastAPI)**
```bash
cd server-python
pip install --require-hashes -r requirements.lock
FCAPTCHA_SECRET="$(openssl rand -hex 32)" python server.py
```

**Node.js (Express)**
```bash
cd server-node
npm install
FCAPTCHA_SECRET="$(openssl rand -hex 32)" node server.js
```

### 2. Add to Your Site

Existing deployments: read the [hardening and upgrade notes](HARDENING.md) before
upgrading signing keys, IP-bound tokens, or browser error handling.

**Where the widget comes from**

Two options, and the tradeoff is real:

```html
<!-- Self-hosted: served by your FCaptcha server, same-origin. -->
<script src="https://your-server.com/fcaptcha.js"></script>

<!-- CDN widget: still requires a running FCaptcha API. -->
<script
  src="https://cdn.jsdelivr.net/npm/@webdecoy/[email protected]/dist/fcaptcha.min.js"
  integrity="sha384-…"
  crossorigin="anonymous"></script>
```

Self-hosting stays the default and is what every server does out of the box: no
third party sees your visitors, and there is no external dependency to fail. The
CDN build simplifies serving the widget; it does not replace the API. Configure
`FCaptcha.serverUrl` for your running server when it is on a different origin.

If you use the CDN, **pin the version and use the integrity hash**. The digest
for each release is published in that release's notes and in
`dist/integrity.json` inside the package; `npm run integrity` in `client/` prints
it for any local build. Without `integrity`, a compromised CDN can replace your
captcha with anything it likes.

The minified bundle is approximately 74 KB, 19 KB over the wire with Brotli. Run `npm run build` in `client/` for exact sizes and integrity hashes.

**Checkbox Mode (Interactive)**

```html
<script src="https://your-server.com/fcaptcha.js"></script>
<div id="captcha"></div>

<script>
  FCaptcha.configure({ serverUrl: 'https://your-server.com' });

  FCaptcha.render('captcha', {
    siteKey: 'your-site-key',
    callback: (token) => {
      document.getElementById('token').value = token;
    }
  });
</script>
```

A widget inside a `<form>` also adds a hidden `fcaptcha_token` input, so the
token is submitted with the form and the callback is optional. Rename it with
`responseFieldName` (`data-response-field-name`) or turn it off with
`responseField: false` (`data-response-field="false"`). If the form already has
a field with that name, the widget leaves it to you and adds none.

**Language**

The widget picks a language automatically: an explicit `lang` option, else
`data-lang` on the container, else the page's `<html lang>`, else the browser's
`navigator.language`, else English. The page outranks the browser deliberately —
a widget should match the form around it rather than the reader's locale.

```html
<!-- nothing to configure: inherits from the page -->
<html lang="de"> … <div id="captcha"></div>

<!-- or state it -->
<script>FCaptcha.render('captcha', { siteKey, lang: 'ja' });</script>

<!-- or per element, for the auto-rendered form -->
<div data-fcaptcha="your-site-key" data-lang="pt-BR"></div>

<!-- or site-wide -->
<script>FCaptcha.configure({ serverUrl: '…', lang: 'fr' });</script>
```

Region tags fall back to their base language, so `de-AT` resolves to `de`;
`pt-BR` and `zh-TW` have their own entries where the wording differs. Arabic,
Hebrew and Persian render right-to-left.

Shipped: `ar bg ca cs da de el en es fa fi fr he hi hu id it ja ko ms nb nl pl
pt pt-BR ro ru sk sv th tr uk vi zh-CN zh-TW` — `FCaptcha.languages()` returns
the live list.

Not on the list, or want different wording? Override any of the five strings —
no fork required:

```js
FCaptcha.render('captcha', {
  siteKey,
  lang: 'is',
  strings: {
    label: 'Ég er ekki vélmenni',
    verifying: 'Staðfesti…',
    verified: 'Staðfest',
    failed: 'Staðfesting mistókst',
    retry: 'Staðfesting mistókst. Reyndu aftur.',
  },
});
```

Overrides are HTML-escaped, so a string coming from a CMS or locale file cannot
turn the widget into an injection point.

For pages that mount and unmount widgets, call `FCaptcha.destroy(widgetId)` to
release listeners, sensors, timers, and workers. Invisible sessions expose
`session.destroy()`; the one-shot `FCaptcha.execute()` cleans itself up.

**Invisible Mode (Zero-Click)**

```html
<script src="https://your-server.com/fcaptcha.js"></script>

<script>
  FCaptcha.configure({ serverUrl: 'https://your-server.com' });

  // Auto-protect all forms
  FCaptcha.invisible({
    siteKey: 'your-site-key',
    autoScore: true
  });

  // Or manually score specific actions
  const result = await FCaptcha.execute('your-site-key', {
    action: 'login'
  });

  if (result.success && result.token) {
    // Send the token to your backend for server-side verification.
  }
</script>
```

**React (no library required)**

The widget exposes a global API, so a small hook is all you need — no wrapper package to install or maintain.

```jsx
import { useEffect, useState, useCallback, useRef } from 'react';

function useFCaptcha({ serverUrl, siteKey }) {
  const [ready, setReady] = useState(typeof window !== 'undefined' && !!window.FCaptcha);

  useEffect(() => {
    if (window.FCaptcha) {
      window.FCaptcha.configure({ serverUrl });
      setReady(true);
      return;
    }
    const script = document.createElement('script');
    script.src = `${serverUrl}/fcaptcha.js`;
    script.async = true;
    script.onload = () => {
      window.FCaptcha.configure({ serverUrl });
      setReady(true);
    };
    document.head.appendChild(script);
    return () => { script.remove(); };
  }, [serverUrl]);

  const execute = useCallback(
    (action) => window.FCaptcha.execute(siteKey, { action }),
    [siteKey]
  );

  return { ready, execute };
}

// Invisible mode — get a token at submit time
function LoginForm() {
  const { ready, execute } = useFCaptcha({
    serverUrl: 'https://your-server.com',
    siteKey: 'your-site-key',
  });

  async function onSubmit(e) {
    e.preventDefault();
    const { token } = await execute('login');
    await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: e.target.email.value, fcaptchaToken: token }),
    });
  }

  return (
    <form onSubmit={onSubmit}>
      <input name="email" type="email" required />
      <button disabled={!ready}>Sign in</button>
    </form>
  );
}

// Checkbox mode — render the interactive widget
function FCaptchaCheckbox({ siteKey, serverUrl, onVerify }) {
  const ref = useRef(null);
  const { ready } = useFCaptcha({ serverUrl, siteKey });

  useEffect(() => {
    if (!ready || !ref.current) return;
    const widgetId = window.FCaptcha.render(ref.current, { siteKey, callback: onVerify });
    return () => window.FCaptcha.destroy(widgetId);
  }, [ready, siteKey, onVerify]);

  return <div ref={ref} />;
}
```

The same pattern works in Vue, Svelte, Solid, and Angular — the widget is framework-agnostic. If you'd rather not write the glue, opening an issue describing how you want to consume it helps us decide whether to ship an official wrapper.

### 3. Verify on Your Backend

**Already using Turnstile, reCAPTCHA or hCaptcha?** Point the verification call
you already have at your FCaptcha server — the contract is identical:

```diff
- https://challenges.cloudflare.com/turnstile/v0/siteverify
+ https://your-server.com/turnstile/v0/siteverify
```

```bash
curl -X POST https://your-server.com/turnstile/v0/siteverify \
  -d "secret=your-secret" -d "response=$TOKEN"
# {"success":true,"challenge_ts":"...","hostname":"example.com","action":"login",

Frequently Asked Questions

What is FCaptcha?

FCaptcha is a Security library for the Go programming language. Self-hosted CAPTCHA server and Go client that detects bots, AI agents, and headless browsers through behavioral analysis and proof of work

How do I install FCaptcha?

Install FCaptcha with the Go module system using `go get WebDecoy/FCaptcha`. Check the repository for the current installation instructions.

What category does FCaptcha belong to?

FCaptcha is listed under Security, specifically Security.

← Back to Security