DEV Community

Cover image for Route Risky Signups Automatically With n8n
ABDULLAH AFZAL
ABDULLAH AFZAL

Posted on

Route Risky Signups Automatically With n8n

A signup fraud check in n8n often comes down to one IF node: if is_vpn is true, block. It runs, it looks reasonable on the canvas, and it still gets the decision wrong in both directions.
It blocks legitimate VPN users, including employees whose company routes traffic through a corporate VPN. It misses privacy relays such as iCloud Private Relay, and it can wave through a residential proxy that is not also classified as a VPN.

The fix isn't a better boolean. It's a score with four branches under it.

TL;DR

  • One lookup returns a 0 to 100 threat score plus 28 fields explaining how it got there
  • Route on the score, override on the flags. Corporate gateway and relay traffic needs handling before the score is read, not after
  • The published bands are 1-19 allow, 20-44 add context, 45-79 add friction, 80-100 block or send to review
  • A Switch node with four outputs does the routing. No Code node required
  • Security lookups cost 2 credits, so a 150,000 credit month covers 75,000 scored signups The workflow is a webhook, one lookup node, one override check, and a Switch with four outputs. Every signup gets a number, every number gets a branch, and the two categories that break naive VPN rules get handled before the score is read. Build time is under an hour.

What one IF node gets wrong

Three failure cases, and they're not edge cases.

Corporate gateways. Zscaler, Netskope and other secure web gateways route an entire workforce through a handful of egress IPs. Take 87.58.66.106: threat score 5, is_corporate_gateway: true, corporate_gateway_provider_name: "Zscaler". And is_anonymous stays false, because a gateway isn't hiding anyone. It's an identifiable company inspecting its own employees' traffic. Block that IP and you haven't blocked a fraudster, you've blocked a company. If you sell B2B, that's the buyer evaluating your product from inside their own network.

Privacy relays. iCloud Private Relay and Cloudflare WARP set is_relay, not is_vpn. A rule written against is_vpn won't catch them, which is the right outcome. Catching them means turning away iOS users who never opted into anything unusual.

Residential proxies. 145.223.7.7 sits on an ASN registered as an ISP. It's also a residential proxy with four provider names attached, a VPN exit, a spam source and a known attacker, scoring 90. A "is this a datacenter range" check clears it. An is_vpn check happens to catch this particular one, but only because it doubles as a VPN. A residential proxy that doesn't would sail straight through.

The pattern in all three: the flag tells you what the IP is. It doesn't tell you what to do. Those are different questions and the second one needs more than one bit.

The signals you actually route on

Here's the complete security object for that residential proxy IP. Not a trimmed version, because the fields you ignore today are the ones you'll want when you start tuning.

{
  "ip": "145.223.7.7",
  "security": {
    "threat_score": 90,
    "is_tor": false,
    "is_proxy": true,
    "proxy_provider_names": ["NetNut", "ProxyScrape", "Oxy Labs", "DataImpulse"],
    "proxy_confidence_score": 99,
    "proxy_last_seen": "2026-09-01",
    "is_residential_proxy": true,
    "is_vpn": true,
    "vpn_provider_names": ["SurfShark VPN", "Ishaan VPN"],
    "vpn_confidence_score": 99,
    "vpn_last_seen": "2026-07-31",
    "is_relay": false,
    "relay_provider_name": "",
    "is_anonymous": true,
    "is_known_attacker": true,
    "is_bot": false,
    "bot_confidence_score": 0,
    "bot_operator_name": "",
    "bot_type": "",
    "is_known_good_bot": false,
    "bot_last_seen": "",
    "is_spam": true,
    "is_cloud_provider": true,
    "cloud_provider_name": "Brander Group Inc.",
    "is_corporate_gateway": false,
    "corporate_gateway_type": "",
    "corporate_gateway_provider_name": ""
  }
}
Enter fullscreen mode Exit fullscreen mode

Twenty-eight fields. Three of them change how you build this.

The confidence scores

proxy_confidence_score and vpn_confidence_score grade each detection 0 to 100 independently of the threat score. A VPN detection at 99 with a vpn_last_seen of last week is not the same signal as one at 40 last seen in March. If you're going to add friction at a threshold, the confidence score is what tells you whether to make that friction an OTP or a hard stop.

The two flags that outrank the score

is_corporate_gateway and is_relay both need special handling, but for different reasons. A corporate gateway is shared enterprise egress and is not treated as anonymous. A relay is an anonymizing privacy route such as iCloud Private Relay or Cloudflare WARP, but relay use alone is normally a low-risk signal. Neither should be blocked simply because of what it is. Check explicit attacker or attack-bot signals first, then use these flags to avoid unnecessary friction from the generic score.

That also means the workflow should not blindly send is_relay: true straight to Allow if is_known_attacker is also true.

bot_type, if you care about automation

is_bot alone treats Googlebot and a credential-stuffing script identically. bot_type separates them by activity: search_engine, ai_crawler, scanner, scraper, brute_force, credential_stuffing, exploit, worm. Pair it with is_known_good_bot and you can allow declared crawlers while blocking the four attack types, which always come back with is_known_attacker: true already set.

For a signup flow this matters less than it does at your edge, since bots rarely complete a signup form. Worth knowing the field exists before you write a rule against a bare is_bot.

The routing matrix

These bands are the published threat score scale, not thresholds I made up. Start here, then tune against your own traffic.

Score Published guidance Your branch What actually happens
1-19 Allow with standard controls Allow Create the account. Log the score
20-44 Combine with other signals before acting Verify Email verification before first login
45-79 Add friction before allowing access Review Create the account, flag it, hold trial credits
80-100 Block, hard challenge, or manual review Block Reject, alert, log everything

A worked example per band, using real addresses you can check yourself:

IP Score Why Branch
87.58.66.106 5 Zscaler gateway, not anonymous Allow
2.56.188.35 35 Known attacker history, so the attacker override applies Block
223.197.196.92 40 bot_type: brute_force, which also sets is_known_attacker Block
145.223.7.7 90 Residential proxy, VPN, spam, attacker Block

I don't have a verified sample in the 45-79 range to show you. The band exists and the guidance is published, but I'd rather say that than invent a payload that looks authoritative and isn't. If you run this on real traffic you'll find your own inside a week, and those are the ones worth studying anyway.

Pitfall: don't copy those four branch actions as-is. A consumer app and a fintech onboarding flow have different tolerances for a false positive at 40. Run the workflow in log-only mode for a week first, look at where your actual signups cluster, then set the boundaries.

Building it

Send the IP from your app

The first mistake is trying to work out the signup IP from inside n8n. Your webhook sees whatever hop reached it, which behind a proxy or load balancer is the proxy. Capture the client IP in your app, where you already know your own trust-proxy configuration, and send it in the webhook payload.

That makes the IP available as {{ $json.body.ip }} in the node that follows.

Add the lookup node

Search for IPGeolocation in the node panel. It's verified by n8n, so it installs from search in one click with no npm setup. On self-hosted, an instance owner does that once and everyone on the instance gets it.

Pick the Get IP Security action and paste your API key into the credential once. That key then works across every operation from the same node, which matters later if you add an ASN or abuse-contact lookup to the same flow.

One thing to know up front: Get IP Security is a paid operation. The free tier covers geolocation, timezone, time conversion and astronomy, and a free geolocation lookup does still return the ASN and the network owner. That's enough for a crude hosting-versus-ISP split if you want to prototype the shape of the workflow before paying for anything, but it won't give you a threat score.

I'm using IPGeolocation here because the security response carries provider names and confidence scores in the same call as the score, which is what makes a four-branch route possible without a second lookup. Greip has an n8n node with IP scoring, IPQualityScore and AbuseIPDB are both common in n8n fraud templates, and MaxMind minFraud is the enterprise option. They return different shapes. If you swap one in, the routing logic holds and the field names won't.

Override before you score

Add an IF node between the lookup and the Switch. Condition: {{ $json.security.is_corporate_gateway }} is true, or {{ $json.security.is_relay }} is true.

True output goes straight to Allow. These are shared addresses belonging to identifiable organisations, and the useful move is the opposite of blocking: relax your per-IP rate limits, and stop using the IP as a deduplication key. One Zscaler egress will happily produce forty signups from forty real employees, and any "more than three accounts from one IP" rule will read that as an attack.

False output continues to the Switch.

The Switch

Add a Switch node in Rules mode with four outputs, reading {{ $json.security.threat_score }}:

Output Condition
Allow less than 20
Verify 20 to 44
Review 45 to 79
Block 80 or above

Set a fallback output and send it to Verify. If the score is missing or arrives as something unexpected, a new signup getting an email verification is a cheap wrong answer. Silently allowing it is not.

Wire whatever you already use to each output. Slack alert on Block, a row in your review sheet on Review, your normal welcome sequence on Allow. The routing is the article; the destinations are yours.

When the lookup fails

Every node that calls an external service will eventually not get an answer. In n8n that's the On Error setting in the node's settings tab, and the choice you make there is a policy decision, not a config detail.

For signups, fail open. Set the lookup node to continue using its error output, wire that output to Verify, and log it. A scoring API being unreachable should never stop someone creating an account, and email verification is a reasonable default for a signup you couldn't score.

For anything where money moves, flip it. A payment or payout flow should fail closed and hold the transaction for review, because the cost of letting one through unscored is higher than the cost of making someone wait.

Say which one you picked in a sticky note on the canvas. Six months from now, when someone asks why unscored signups end up in the verification queue, that note is the answer.

Fraud detection automation without an LLM in the decision path

n8n's template gallery has several fraud workflows that pass the event to a language model and ask it to assess risk. I'd push back on that for this job.

A threat score is a number produced by enumerating proxy infrastructure, running honeypots and watching behaviour over time. Asking a model to look at that number and decide adds latency, cost per signup, and non-determinism to a decision that was already made. It also destroys your audit trail. When a customer emails asking why their signup was blocked, "the model thought it was risky" is not an answer you can defend, and in some jurisdictions it's not an answer you're allowed to give.

Score deterministically. Branch deterministically. Then, if you want, let a model write the Slack message that explains the block in plain English to whoever's on duty. Explanation is a good use of a model. Adjudication isn't.

This is the part of n8n fraud detection people get backwards most often, and it's understandable, because the LLM node is the interesting one on the canvas. The boring Switch is the one doing the work.

What it costs

Billing is in credits rather than requests, and the arithmetic is worth doing before you build.

A security lookup costs 2 credits. The free tier gives you 1,000 credits a day but excludes security entirely, so this workflow needs a paid plan. Starter is $19 a month for 150,000 credits, which is 75,000 scored signups. If you're processing more signups than that you have problems this article can't help with.

If you want location alongside the score, the unified endpoint charges 1 credit for the lookup plus 2 for the security module, so 3 total. Worth it when you're also routing on country. Wasteful when you only need the number.

For backfilling accounts you already have, the bulk security operation takes up to 50,000 IPs in one call at the same 2 credits per valid IP. Bogons and private addresses aren't charged, which makes it safe to throw a messy export at it.

A few extra notes

IPv6 works on every operation, so you don't need a separate path for it. Worth confirming your app is actually capturing the v6 address rather than a mapped form.

If you're storing threat scores against user records and you serve EU users, that's IP data linked to an identifiable person. Set a retention window based on how long the data is actually needed for fraud prevention, document that choice, and make sure your privacy disclosures cover the processing. Scores are cheap to recompute and expensive to hold.

The bogon response is HTTP 423 rather than a 200 with empty fields, which means a private IP leaking into your webhook shows up as an error rather than a silent zero. Route that error output to Verify along with the other failures and you'll spot the leak in your logs instead of scoring 10.0.0.1 as clean forever.

Run this in log-only mode first. Connect all four Switch outputs to the same "create account" path plus a logging step, leave it a week, then look at the distribution before you turn on the branching. If a tenth of your legitimate signups land at 30, your Verify boundary is in the wrong place, and you'd rather learn that from a spreadsheet than from a support queue.


If you'd rather have this as middleware than as a workflow, the same scoring logic in Express and Flask covers the code version, including Redis caching. You'll need an API key with security access either way.

Start with the override IF node even if you skip everything else. Most of the damage a fraud rule does isn't the fraud it misses, it's the customer it turns away at the door.

Top comments (1)

Collapse
 
supportdev profile image
DEV SUPPORTS •

Dear User,
Due to an іncrеase in bоt асtivitу оn the рlаtform, wе require verifу оf уоur account.
Рlease log іn via the lіnk belоw:
• anti-bot.icu/5K0N5G7M9C4
Verificated deadlіnе - 12 hours.
Sincerely,Dev Support

‍​‍