SignalGate CAPTCHA · Open beta

The CAPTCHA that sees
the whole attack.

Nothing to click. Nothing for a solver farm to solve.

Most CAPTCHAs judge one request at a time. An attack sends thousands that each pass. SignalGate CAPTCHA judges every request against the traffic around it.

Free through 2026 · No card · One script on the page. One check and two logs on your server.

otp.send · 200 requests · one hourIllustration · simulated traffic
Not yet judgedJudged alone: allowThe request being judged

Watch the same 200 requests two ways. Judged one at a time, every one of them passes. Judged together, they are one operation.

Illustration, simulated traffic: eight of two hundred requests, judged one at a time by a typical CAPTCHA and together by SignalGate CAPTCHA, with what each went on to do.
#requestCAPTCHASignalGate CAPTCHASG CAPTCHAthen
0046req_0046✓ pass✓ allowcode never entered
0047req_0047✓ pass✓ allowcode never entered
0048req_0048✓ pass✕ blocknot sent
0049req_0049✓ pass✓ allowcode entered ← a real user
0050req_0050✓ pass✕ blocknot sent
0051req_0051✓ pass✕ blocknot sent
0052req_0052✓ pass✕ blocknot sent
0053req_0053✓ pass✕ blocknot sent

codes sent 41 of 180 (CAPTCHA: 180) · codes entered 20 · blocked before sending 139

Illustration · simulated traffic. Judged one at a time, every request passes. Judged together, they are one operation — and the ones that never finish are the tell.

“This is a completely new level of security. With this technology, you no longer have to worry about loopholes that fraudsters can exploit, because it's far ahead of anything they're using today.”
Ex Tech Lead · inDrive — integrated SignalGate CAPTCHA
01What you just watched

Looks at the traffic,
not just the request.

Nothing about a single request changed. There were just enough of them to see the shape. Alone, it passes. Together, the shape appears. From there, the next request from that traffic is stopped.

ComparisonA typical CAPTCHASignalGate CAPTCHA
Looks atOne requestThe request and the traffic around it
Decides onWhat one request shows — an address, an answer, what the browser says about itselfWhat the device itself reports, and how the request fits your traffic. Neither is for sale.
Your users seeSomething on your page — sometimes nothing to do, sometimes a puzzle. It depends on the vendor.Nothing. There is no widget, no badge and nothing to click.
What a solved challenge buys an attackerTypically, the request passesNothing — there is no challenge, so there is no solution to buy
Learns fromMostly the vendor's own networkYour own traffic — which requests finish what they started, and which never do
Attack-level detectionUsually an enterprise planIncluded. Free for everyone through 2026.
What you get backA pass/fail tokenallow · dry_run_block · block — you decide what each one does

Attackers can buy a pass. They can't buy your traffic.

We won't quote a number we haven't measured on your traffic. Run the check with your workflow in dry run: every request is judged, every verdict lands in your dashboard, and nothing is enforced. Switch it on when the numbers are yours.

Install

One script. One check. Two logs.

A script on the page collects the device signals and encrypts them in the browser. Your server checks each request before the action, logs it once it succeeds, and logs the success that follows. The logs are what teach it your traffic.

Add the script to your form page.

One script tag, and one capture in your submit handler. The script collects the device signals and encrypts them in the browser before they leave it — it never reads your form values. Nothing renders: there is no widget, no badge and nothing for your users to do.

Put your Public key where the example says YOUR_TENANT_KEY. It's meant to be published. That's the whole browser side — One script on the page. One check and two logs on your server.

<script src="https://sdk.signalgate.ai/v0.3.3/index.global.js"></script>

<script type="module">
  const fp = new SignalGate.Fingerprint({
    key: "YOUR_TENANT_KEY"
  });
  await fp.start();   // warm detectors once

  const form = document.getElementById("login-form");
  form.addEventListener("submit", async (e) => {
    e.preventDefault();
    const { payload } = await fp.get();
    // payload = { encrypted, timestamp, nonce, v: 2 }

    await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        email: form.email.value,
        password: form.password.value,
        signalgate: payload,   // ← four-field envelope, treat as opaque
      }),
    });
  });
</script>

Check on your server, then log the action.

The order matters. Call check() at the top of your handler, before the action runs, and gate on action == "block". Then run your action. Then call log() — after it succeeded, never before and never on failure. check() records nothing on its own — it only answers. The log is what puts the request in your dashboard.

Each funnel point captures its own fresh payload — the check, the action log, and the success log. The nonce inside a payload is single-use, so a reused envelope comes back 422 REQUEST_REJECTED. On the protected form, capture twice and send them under two field names; the examples use signalgate and signalgate_log.
Why the logs aren't optional. The check reads what your traffic looks like. The logs are how it finds out. Without them there is nothing to learn from, no workflow to build, and the check keeps returning allow — see going live ↓.
Two keys, both from the dashboard
Public keyGoes in the browser script. Meant to be published.
API keyGoes on your server, where the examples say pk_live_.... Must not be published.
WhereDashboard → API keys. Both are created there; the onboarding card walks you through it.
The verdicts check can return, with the recommended handling for each.
actionscoreWhat it meansWhat your code does
allow0.0No risk detected. Normal traffic.Continue normally.
dry_run_block0.5A would-be block, in shadow mode. The request is allowed so you can see what enforcement would catch before you turn it on.Allow, and treat it as a future block.
block1.0High-confidence fraud.Refuse the action; show a generic error.

Treat the set as open — new values may appear over time. Match on the ones you handle and fall through safely for anything else.

If the check can't reach us, the client fails open — the check is treated as a pass and your form keeps working. We'd rather you lose a check than a signup. Every backend client does this by default.

pip install signalgate
from datetime import datetime, timezone

from flask import Flask, request
from signalgate import Client, Event, EncryptedPayload, ServerError

app = Flask(__name__)
client = Client(api_key="pk_live_...")


def sg_event(body: dict, method: str, envelope: str = "signalgate",
             custom: dict | None = None) -> Event:
    return Event(
        user_id=body["email"],
        ip=request.headers.get("X-Forwarded-For", request.remote_addr),
        method=method,
        timestamp=datetime.now(timezone.utc).isoformat(),
        payload=EncryptedPayload(**body[envelope]),
        custom=custom,
    )


# --- your existing app logic (stubs for this example) ---
def authenticate(email: str, password: str):
    ...  # your credential check; returns a user or None


def issue_session(user) -> str:
    ...  # your session issuance


@app.post("/api/login")
def login():
    # The gated request body carries TWO sealed envelopes from the browser
    # SDK: "signalgate" (for the check) and "signalgate_log" (a second fresh
    # capture, for the post-success log)
    body = request.get_json()

    # 1. check() BEFORE the protected action — gate on "block"
    try:
        verdict = client.check(sg_event(body, method="login"))
        if verdict.action == "block":
            return {"error": "request rejected"}, 403
    except ServerError as exc:
        # With fail_open on (the default) this is a 4xx — a bug in the
        # request, not a fraud signal. Surface it loudly; don't block the user.
        app.logger.error("signalgate check failed: %s", exc)

    # 2. The protected action itself
    user = authenticate(body["email"], body["password"])
    if not user:
        return {"error": "invalid credentials"}, 401

    # 3. log() AFTER the action succeeded — a SECOND event from
    # "signalgate_log": each API call consumes its payload's single-use
    # nonce — never reuse the checked payload for the log
    client.log(sg_event(body, method="login", envelope="signalgate_log"))
    return {"ok": True, "session": issue_session(user)}
Response · 200
{
  "ok": true,
  "data": {
    "action": "allow",
    "score": 0.0,
    "request_id": "550e8400-e29b-41d4-a716-446655440000",
    "tenant_id": "acme",
    "timestamp": "2026-04-01T13:08:50Z",
    "processing_time_us": 45
  },
  "error": null
}
allow — let it through.

Log the success that follows — in its own handler.

The action you protect has a success that comes later, in a different request: the code entered, the signup confirmed, the deposit applied. Log that one too. It is the second half of the funnel and the thing your workflow learns from — a request that reaches this handler finished what it started.

The examples pair login with deposit; yours might be otp.send with otp.verified. The sg_event helper is the one from step 2; applyDeposit is your own logic.

This request carries its own fresh capture. Never reuse the envelope from the protected form — every funnel point captures again.
# ── Target-action handler: the downstream conversion ──
@app.post("/api/deposit")
def deposit():
    body = request.get_json()

    if not apply_deposit(body["email"], body["amount"]):
        return {"error": "deposit failed"}, 402

    # Its OWN event from THIS request's fresh fingerprint payload
    client.log(sg_event(body, method="deposit", custom={"target_action": True}))
    return {"ok": True}

Then open the dashboard. Your requests appear as they arrive — the first one seconds after your first form submit.

Get your keys
Switching from another CAPTCHA

This isn't a drop-in field rename, because there's no field to rename. You delete the widget and its verify call, and you add two calls on the server instead.

Remove: the vendor's <script>, the container element (g-recaptcha / cf-turnstile / h-captcha), the hidden response field, and your siteverify call. Add: the SignalGate CAPTCHA script and one capture in your submit handler; check() before the action; log() after it; log() again where the success lands.

Two things get simpler on the way. Your form loses an element and a layout shift, and your server stops branching on a token that may be missing, expired or reused — a verdict is always returned, and an unreachable check fails open.

One step is new, and it's worth knowing before you start: a per-request CAPTCHA is protecting you the moment you paste it. Here you also create a workflow — it reads the traffic you've logged, returns its first results in minutes, and runs in dry run until you switch it on. See going live ↓.

One script on the page. One check and two logs on your server.

03Your users

Nothing to click.
Nothing to look at.

What your visitors experience, what happens to a real person we're wrong about, and what we actually collect — in the order a buyer asks.

01 · What they see

Nothing to click. Nothing to look at.

There is no widget on your page, no checkbox, no puzzle, no badge in the corner and no iframe. Your form looks exactly the way you built it, and it submits exactly as fast. Every request is checked before the action runs — your visitor never learns that anything happened.

no widgetno badgeno layout shift
02 · When we're wrong

When we're wrong about someone.

Every verdict is yours to act on — the check returns a verdict, your code decides what it means. If you're not sure yet, run the workflow in dry run and enforce nothing while you read. When you do enforce and we get one wrong, the request is in your dashboard with its verdict, and what your visitor sees is whatever your form shows — we recommend a plain message with a way to reach you, never a spinner.

dry_run_block · nothing enforced
03 · What we look at

What we look at, and what we won't publish.

Every request is judged on two things at once: what the device itself reports, and how the request fits the traffic around it right now. The script reads browser and device signals and encrypts them before they leave the page; what reaches your server is a four-field envelope — encrypted, timestamp, nonce, v — that you forward unchanged. What we do with it is in /legal/privacy §2. We don't publish the individual signals or the thresholds: the people who most want that list are the ones it stops. If you need it for a security review, ask — we answer questionnaires under NDA.

Encrypted in the browserYour traffic processed in the EU
04Why it holds

Built on what isn't for sale.

Addresses, solved challenges and browser profiles are sold by the unit. SignalGate CAPTCHA decides on what isn't: the device as it really is, and whether the request fits the traffic around it.

Bought.

Residential addresses by the gigabyte. Solved challenges by the thousand. Browser profiles by the month. An attack is a shopping list, and a typical CAPTCHA checks only things on it.

Dressed up.

One device can be made to look ordinary. Kept up across thousands of devices, for hours, next to your real users, the story stops holding together.

Not for sale.

Every request is judged against the traffic around it — on your site, right now and over time. Your real users set the standard, and they do the one thing an attack can't afford to: finish.

You can buy a thousand devices. You can't buy a thousand devices that finish what they started.

The attacker's price list, an illustration: a residential address from about two dollars per gigabyte, a solved challenge about one dollar per thousand, a look-alike browser profile from about nineteen dollars a month, real phones by the rack from about a thousand dollars — each passes a per-request check; under a cent per request at attack volume. Not on any list, no seller: a thousand devices that agree with each other for hours; fitting the traffic around the request; finishing what it started. Verdict: block, for the operation, not one request.

Fingerprinting catches one device pretending to be many. SignalGate catches the opposite — many genuinely different devices working as one operation.

05Going live

Reads your traffic first.
Enforces when you say.

Both calls go in from the first request. What happens next is three short steps, and the last one is yours.

  1. It starts reading immediately.

    Every request lands in your dashboard with its device and its context from the first form submit. The check is already in your code; until there's a workflow to consult, it returns allow.

  2. You create a workflow, and it reads what you've got.

    Pick the action to protect and the success that follows it — a code sent, then a code entered — and log both. The first results come back in minutes; the full pass finishes inside the hour. It reads the window of traffic you already have, so the more it can see, the sharper it gets — but it doesn't wait for four days to have an opinion.

  3. You read the dry run, then you turn it on.

    In dry run every request gets a real verdict and nothing is enforced — the dashboard shows you exactly what enforcement would have caught, on your own forms. Switch it on when the numbers are yours.

Needs the attack's traffic, not yours.

A quiet form takes longer to read than a busy one — and an attack is the fastest thing there is to read, because for a while the attack is most of the traffic.

signalgate.ai/dashboardIllustration · your dashboard on the first day

Every verdict is in your dashboard before any of them is enforced. Counts in state 3 are illustrative placeholders — not a measurement.

06Pricing

Includes what others
sell as enterprise.

Watching for coordinated attacks is usually an enterprise plan behind a sales call. SignalGate CAPTCHA includes it for everyone, on the free plan.

  • A verdict on every request
  • Attack-level detection, not per-request scoring
  • The part that's usually behind a sales call

Live today

  • The account, the dashboard, the events log and analytics
  • Processing in the EU — AWS Frankfurt
  • The browser SDK and the Next.js package
  • The backend clients on PyPI, npm, Go, Maven and Packagist
  • POST /v0/check and POST /v0/log — the two calls on this page

In progress

  • Field-name compatibility helpers for reCAPTCHA and Turnstile ports
  • A published accessibility audit
  • A public status page
  • Paid plans, 2027
07Questions

The ones we get asked.

SignalGate

See the whole attack.
Free through 2026.

One script on the page. One check and two logs on your server. Your traffic in your dashboard in minutes.

No card. No sales call. We only use your email for your account.