Keep your CAPTCHA.
SignalGate adds no widget and no step for your users, so it runs beside what you have and judges what gets through.
A CAPTCHA judges one request, a rate limit one address, a device check one device — each cheap to get past. SignalGate judges requests against the traffic around them, and learns which visitors finish what they start. Fraud rarely does.
Watch the same requests judged alone, then judged with the traffic around them.
The same attack over seven days, with defences to choose from: play the demo game →
200 passed. 180 never entered the code. 180 codes paid for.
One pattern. 180 requests. 139 blocked before the code was sent.
Same request, no traffic around it: allow. The verdict was never about this request alone.
Watch the same 200 requests two ways. Judged one at a time, every one of them passes. Judged together, they are one operation.
| # | request | alone | SignalGateSignalGate | then |
|---|---|---|---|---|
| 0046 | req_0046 | ✓ pass | ✓ allow | code never entered |
| 0047 | req_0047 | ✓ pass | ✓ allow | code never entered |
| 0048 | req_0048 | ✓ pass | ✕ block | not sent |
| 0049 | req_0049 | ✓ pass | ✓ allow | code entered ← a real user |
| 0050 | req_0050 | ✓ pass | ✕ block | not sent |
| 0051 | req_0051 | ✓ pass | ✕ block | not sent |
| 0052 | req_0052 | ✓ pass | ✕ block | not sent |
| 0053 | req_0053 | ✓ pass | ✕ block | not sent |
codes sent 41 of 180 (alone: 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.
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.
| Comparison | A typical CAPTCHA | SignalGate |
|---|---|---|
| Looks atLooks at | One request at a timeOne request at a time | Requests in bulk — each one against the traffic around itEach request against the traffic around it |
| Decides onDecides on | What one request shows: an address, an answer, what the browser says about itselfWhat one request shows: an address, an answer, what the browser says | Whether traffic like it goes on from the sensitive action to the target action — a code sent to a code entered, a trial to a paid planWhether traffic like it goes on to the target action — a code entered, a paid plan |
| When attackers adaptWhen attackers adapt | A solving service passes the challenge for centsA solving service passes the challenge for cents | It adapts with them. There is no challenge, so a solver has nothing to solveIt adapts with them. No challenge, so nothing to solve |
| Your users seeYour users see | A widget — sometimes a puzzleA widget — sometimes a puzzle | Nothing. No widget, no badge, nothing to clickNothing. No widget, nothing to click |
We don't guess what it will block for you. Preview a workflow with Dry Run: the dashboard estimates the effect on your traffic and your conversion before anything is blocked.
otp.send and otp.verified. Both go in with log().check() returns a verdict before the action runs: allow, or the action you chose — dry_run_block while you watch, block when you enforce.SignalGate adds no widget and no step for your users, so it runs beside what you have and judges what gets through.
Backend SDKs fail open by default: a timeout (3 seconds, configurable), a network error or a server error counts as allow.
Start the workflow with dry_run_block: real verdicts land in Events and nothing is blocked. Switch to block when the numbers convince you.
A block is a verdict your code acts on: refuse with a plain message and a way to reach you, or send the request to a slower check.
Under 10 ms of processing per check, before your network round trip. Every check response reports processing_time_us, so you can see it on your own traffic.
One script on the page and two log() calls on your server — about ten minutes to start logging. Later, one check() before the action.
A request without the browser payload can't be judged: check() rejects it and your code decides. Once blocking is on, treat such requests as suspect.
A campaign too small to stand out from your own traffic, and a brand-new one until SignalGate has adapted to it.
Device signals are encrypted before they leave the page. Only our API can decrypt them.
Your users' data protects your traffic and fills your dashboard. Nothing else.
If our API can't be reached, backend SDKs let the request through. Your forms keep working.
DPA, security questionnaire, data-residency terms — we work through your legal and security requirements with you.
Log for free, preview a workflow with Dry Run and see the predicted effect on your conversion before you block anyone.
A modelled OTP flood or AI free-trial farm, day 1 to day 7.
A sample app you attack and defend with the same log() and check() calls. macOS or Linux.
Free until December 31, 2026 · About ten minutes to start logging · No card required