<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: FindIP</title>
    <description>The latest articles on DEV Community by FindIP (@findip).</description>
    <link>https://dev.to/findip</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4179492%2F9f6e6641-0bba-4bbf-8680-2c67339fa86b.png</url>
      <title>DEV Community: FindIP</title>
      <link>https://dev.to/findip</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/findip"/>
    <language>en</language>
    <item>
      <title>App Attest and Play Integrity won't catch a real phone behind a proxy</title>
      <dc:creator>FindIP</dc:creator>
      <pubDate>Sat, 31 Oct 2026 14:08:31 +0000</pubDate>
      <link>https://dev.to/findip/app-attest-and-play-integrity-wont-catch-a-real-phone-behind-a-proxy-1i2k</link>
      <guid>https://dev.to/findip/app-attest-and-play-integrity-wont-catch-a-real-phone-behind-a-proxy-1i2k</guid>
      <description>&lt;p&gt;App Attest and Play Integrity answer one question: is this my genuine app on a genuine device? That's worth knowing, and you should use them. But a real, unmodified app on a real phone passes, whether that phone is on a home connection or behind a proxy in a rack of phones.&lt;/p&gt;

&lt;p&gt;So if you're fighting fake signups, bonus farming or account takeover, you need a second question: what network is this request coming from?&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not check on the device?
&lt;/h2&gt;

&lt;p&gt;You can look for a VPN from inside the app. On Android, &lt;code&gt;NetworkCapabilities.hasTransport(TRANSPORT_VPN)&lt;/code&gt; tells you about a VPN on the device. It's fine for showing a message, but it won't help against fraud, for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app is reporting on itself. A modified client can answer whatever it likes.&lt;/li&gt;
&lt;li&gt;It can't see a proxy further upstream.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The check belongs on a server, which sees the IP the request actually came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look at
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network type.&lt;/strong&gt; Hosting and datacenter networks are rarely a phone on a carrier or home connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known exits.&lt;/strong&gt; VPN exit nodes, open proxies and Tor exits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changes in a session.&lt;/strong&gt; The IP, network or country switching mid-session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is proof. iCloud Private Relay, corporate VPNs and carrier NAT are all normal for honest users. So treat the result as risk: let most people through, and ask for an extra step when it's high.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the pieces fit
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Network scoring&lt;/th&gt;
&lt;th&gt;App Attest&lt;/th&gt;
&lt;th&gt;Play Integrity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Answers&lt;/td&gt;
&lt;td&gt;Is the network behind this action risky?&lt;/td&gt;
&lt;td&gt;Is this my genuine app on a genuine Apple device?&lt;/td&gt;
&lt;td&gt;Is this my genuine app, from Google Play, on a certified device?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Misses&lt;/td&gt;
&lt;td&gt;Whether the app is genuine&lt;/td&gt;
&lt;td&gt;The network&lt;/td&gt;
&lt;td&gt;The network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decided by&lt;/td&gt;
&lt;td&gt;Your backend&lt;/td&gt;
&lt;td&gt;Your backend&lt;/td&gt;
&lt;td&gt;Your backend&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run them side by side. The rule for all three is the same: your backend decides, never the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  With FindIP Shield
&lt;/h2&gt;

&lt;p&gt;Disclosure: we build this. FindIP Shield does the network part, and it's free during the preview. The SDKs are new, so no production apps use them yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS.&lt;/strong&gt; Add the Swift package (iOS 15+, no dependencies):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/findip-net/findip-shield-swift"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"1.0.3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, on the login button:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;verdict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="kt"&gt;FindIPShield&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ShieldEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loginAttempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"login"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Send verdict.sessionId with the login request; your backend verifies it.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Android.&lt;/strong&gt; Add the library (Android 8+, &lt;code&gt;INTERNET&lt;/code&gt; permission only):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"net.findip:shield-android:1.0.3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, inside a coroutine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;verdict&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FindIPShield&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ShieldEvents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LOGIN_ATTEMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"login"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Send verdict.sessionId with the login request; your backend verifies it.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Your backend&lt;/strong&gt; then asks Shield about that session with the secret key and decides. The key in the app is public and proves nothing. See &lt;a href="https://www.findip.net/docs/shield/server-verification?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=shield_mobile" rel="noopener noreferrer"&gt;server verification&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No answer means unknown, not safe.&lt;/strong&gt; A timeout returns an unknown status, never &lt;code&gt;allow&lt;/code&gt;. Decide per action what to do: a login can usually continue, while a signup might ask for an extra check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing the user typed is sent.&lt;/strong&gt; No form values, advertising IDs, contacts, location or device fingerprint. &lt;a href="https://www.findip.net/docs/shield/mobile?utm_source=dev&amp;amp;utm_medium=article&amp;amp;utm_campaign=shield_mobile" rel="noopener noreferrer"&gt;The full list is in the docs&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this won't catch
&lt;/h2&gt;

&lt;p&gt;Network scoring doesn't recognize the same phone behind a new account, and residential proxies are hard for anyone, because they route through real home connections. Pair it with integrity checks and your own account-level limits.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>android</category>
      <category>security</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Bots, VPNs and fake checkouts on Shopify: what you can see and what you can do</title>
      <dc:creator>FindIP</dc:creator>
      <pubDate>Thu, 22 Oct 2026 15:43:08 +0000</pubDate>
      <link>https://dev.to/findip/bots-vpns-and-fake-checkouts-on-shopify-what-you-can-see-and-what-you-can-do-2nh4</link>
      <guid>https://dev.to/findip/bots-vpns-and-fake-checkouts-on-shopify-what-you-can-see-and-what-you-can-do-2nh4</guid>
      <description>&lt;p&gt;If your Shopify analytics show a spike of sessions from a country you don't ship to, add-to-carts that never convert, or a burst of failed payments at 2 a.m., you're not imagining it. A share of every store's traffic comes from scrapers, price bots, card testers and people hiding behind proxies.&lt;/p&gt;

&lt;p&gt;Shopify handles a lot of this for you. Some of it, it doesn't. Here's what you can see, and what you can actually do about it, mostly with tools you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What Shopify already does
&lt;/h2&gt;

&lt;p&gt;Before installing anything, know what's on by default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Platform-level bot protection.&lt;/strong&gt; Shopify sits behind Cloudflare and filters a lot of abusive traffic before it reaches your store.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hCaptcha on forms.&lt;/strong&gt; It's on by default for contact, comment, login and account-creation forms. Check it's still on under Online Store &amp;gt; Preferences &amp;gt; Spam protection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkout protection.&lt;/strong&gt; Shopify runs its own checks on checkout. Extra bot protection during high-traffic sales is a Shopify Plus feature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fraud analysis on orders.&lt;/strong&gt; Eligible orders get a low, medium or high risk recommendation with indicators, and stores on Shopify Payments also get proxy detection. Shopify's own advice is to go by the recommendation, not by any single indicator such as the IP address.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That covers a lot of card fraud. What it doesn't give you is a clear picture of your &lt;strong&gt;traffic&lt;/strong&gt;: how much of it is people, and how much is automated or anonymized.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why the traffic picture matters
&lt;/h2&gt;

&lt;p&gt;Bot and proxy traffic on a store costs you in quieter ways than fraud:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong numbers.&lt;/strong&gt; Conversion rate, bounce rate and "sessions by country" all include traffic that was never going to buy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wasted ad spend.&lt;/strong&gt; Paid clicks from datacenter networks are rarely shoppers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Card testing.&lt;/strong&gt; Waves of small payment attempts, often from rotating proxies, show up as failed payments before they show up as chargebacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scraping.&lt;/strong&gt; Competitors and resellers pulling your catalog and prices.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tell is usually the &lt;strong&gt;network&lt;/strong&gt; the traffic comes from. Shoppers browse from home broadband and mobile carriers. Bots and scrapers run on cloud servers (AWS, DigitalOcean, OVH, Hetzner) and route through proxies.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What to look at
&lt;/h2&gt;

&lt;p&gt;Whatever tool you use, these are the questions that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What share of &lt;strong&gt;page views&lt;/strong&gt; come from hosting or datacenter networks?&lt;/li&gt;
&lt;li&gt;What share of &lt;strong&gt;checkouts started&lt;/strong&gt; come from VPNs, proxies or Tor, and is that share higher than for page views?&lt;/li&gt;
&lt;li&gt;Do &lt;strong&gt;payment attempts&lt;/strong&gt; from risky networks cluster in bursts? That's what card testing looks like.&lt;/li&gt;
&lt;li&gt;Do spikes in "sessions from country X" line up with a single hosting provider?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll usually find a few clear patterns (a scraper on one cloud provider, a burst of proxy checkouts on one night) rather than a general problem. Shoppers on VPNs are mostly real people, so a VPN on its own tells you little.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What you can do about it
&lt;/h2&gt;

&lt;p&gt;On Shopify, the store front is yours but checkout is Shopify's, so the levers are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What works on Shopify&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Spam on contact and account forms&lt;/td&gt;
&lt;td&gt;Keep hCaptcha on in spam protection settings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Risky orders&lt;/td&gt;
&lt;td&gt;Read the order's fraud analysis, and use Shopify Flow to tag, hold or flag high-risk orders for review&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Card-testing waves&lt;/td&gt;
&lt;td&gt;Watch failed payments by network and time; Shopify's own checks and your payment provider's rules do the blocking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scrapers and junk traffic in analytics&lt;/td&gt;
&lt;td&gt;Identify the networks, then exclude or segment them in your analytics and ad reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Blocking whole countries or VPN users on the storefront&lt;/td&gt;
&lt;td&gt;Blocker apps can do it with a storefront script; test carefully, since real customers use VPNs and checkout itself stays Shopify's&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The general rule holds here too: look before you block. A storefront that blocks every VPN user turns away real customers and still can't touch what happens inside Shopify's checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where FindIP Shield fits
&lt;/h2&gt;

&lt;p&gt;Disclosure: we make FindIP Shield, which does the "what to look at" part for free.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It uses Shopify's Web Pixels API and reports &lt;strong&gt;page views, checkouts started and payment info submitted&lt;/strong&gt;, each scored for VPN, proxy, Tor, hosting or datacenter network and known malicious IPs, with the reasons, in a dashboard.&lt;/li&gt;
&lt;li&gt;It runs only when the shopper has given analytics consent, under Shopify's Customer Privacy controls.&lt;/li&gt;
&lt;li&gt;It's deliberately minimal: no names, emails, addresses, customer or order IDs, cart contents, search queries or payment details are sent, and page URLs are stripped of query strings.&lt;/li&gt;
&lt;li&gt;It reports; it doesn't block shoppers. Because it never sees order IDs, use Shopify's fraud analysis for individual orders and Shield for the traffic picture around them.&lt;/li&gt;
&lt;li&gt;Free up to 10,000 events per site per day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's on the Shopify App Store as &lt;a href="https://apps.shopify.com/findip-shield" rel="noopener noreferrer"&gt;FindIP Shield&lt;/a&gt;, and there's an &lt;a href="https://www.findip.net/shield/demo" rel="noopener noreferrer"&gt;example dashboard with sample data&lt;/a&gt; and a &lt;a href="https://www.findip.net/shopify" rel="noopener noreferrer"&gt;Shopify overview&lt;/a&gt; if you want to see what it shows first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Use what Shopify already gives you: hCaptcha on forms, fraud analysis on orders.&lt;/li&gt;
&lt;li&gt;Find out which networks your traffic comes from: residential, hosting, VPN, Tor.&lt;/li&gt;
&lt;li&gt;Look for patterns (one cloud provider, one burst of failed payments) before blocking anything.&lt;/li&gt;
&lt;li&gt;Act with Shopify Flow on risky orders and segment junk traffic out of your numbers.&lt;/li&gt;
&lt;li&gt;Don't block VPN shoppers outright. Most of them are customers.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>shopify</category>
      <category>ecommerce</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to detect VPN and proxy signups on WordPress (and what to do about them)</title>
      <dc:creator>FindIP</dc:creator>
      <pubDate>Thu, 22 Oct 2026 15:29:52 +0000</pubDate>
      <link>https://dev.to/findip/how-to-detect-vpn-and-proxy-signups-on-wordpress-and-what-to-do-about-them-lmk</link>
      <guid>https://dev.to/findip/how-to-detect-vpn-and-proxy-signups-on-wordpress-and-what-to-do-about-them-lmk</guid>
      <description>&lt;p&gt;If you run a WordPress site with open registration, a contact form or a WooCommerce checkout, you've seen it: a wave of new accounts with throwaway emails, contact-form spam at 3 a.m., orders that look fine until the chargeback arrives. A lot of that traffic doesn't come from home internet connections. It comes through VPNs, open proxies, Tor and rented cloud servers.&lt;/p&gt;

&lt;p&gt;Detecting that is easy. Deciding what to do with it is where most setups go wrong: millions of legitimate people use a VPN every day, and a site that blocks all of them loses real customers.&lt;/p&gt;

&lt;p&gt;Here's how to approach it on WordPress, in the order that keeps false positives low.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Know which network your visitors come from
&lt;/h2&gt;

&lt;p&gt;Every request arrives from an IP address, and that address belongs to a network. The useful question isn't "which IP?" but "what kind of network?":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Residential or mobile ISP&lt;/strong&gt;: home broadband, phone carriers. This is almost everyone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting or datacenter&lt;/strong&gt;: AWS, DigitalOcean, OVH, Hetzner and friends. Real people rarely browse from these; scripts and cheap proxies do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commercial VPN, open proxy, Tor&lt;/strong&gt;: anonymized traffic. Common for privacy-minded users, also common for abuse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known malicious IPs&lt;/strong&gt;: addresses already seen scanning, spamming or attacking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;WordPress doesn't tell you any of this. You need an IP intelligence lookup: either a plugin that calls one, or a layer in front of the site (Cloudflare) that classifies traffic for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Look before you block
&lt;/h2&gt;

&lt;p&gt;The most common mistake is installing a blocker, switching on "block all VPNs", and walking away. Two weeks later a customer emails to say they couldn't check out from their company laptop.&lt;/p&gt;

&lt;p&gt;Run in monitor mode first, for a week if you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What share of &lt;strong&gt;registrations&lt;/strong&gt; come from hosting networks or VPNs?&lt;/li&gt;
&lt;li&gt;What share of &lt;strong&gt;orders&lt;/strong&gt;, and do those orders get refunded or charged back more?&lt;/li&gt;
&lt;li&gt;Are your &lt;strong&gt;contact-form spam&lt;/strong&gt; submissions concentrated on a few networks?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll usually find that the abuse sits in a few clear patterns (datacenter IPs on the registration form, say), while VPN users on checkout are mostly fine. That tells you exactly where to add friction and where to leave things alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Watch out for page caching
&lt;/h2&gt;

&lt;p&gt;If you use a full-page cache (WP Rocket, LiteSpeed Cache, W3 Total Cache, or your host's built-in cache), a PHP check that runs when WordPress renders the page &lt;strong&gt;doesn't run on a cached page&lt;/strong&gt;. The cache serves the stored HTML without WordPress ever looking at the visitor's IP. Server-side blocker plugins usually document this and ask you to exclude protected pages from the cache.&lt;/p&gt;

&lt;p&gt;Two ways around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check at the edge&lt;/strong&gt;: a Cloudflare WAF rule or Worker sees every request before the cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check in the browser and on submit&lt;/strong&gt;: a small script on the page reports the visit and the form submit, so cached pages are covered too. The decision happens when the form is actually submitted.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Protect the forms, not the whole site
&lt;/h2&gt;

&lt;p&gt;Blocking every page for every VPN user is a blunt tool. The forms are where abuse costs you money, so put the friction there:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Typical abuse&lt;/th&gt;
&lt;th&gt;A reasonable response for risky submits&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Registration (&lt;code&gt;wp-login.php?action=register&lt;/code&gt;, membership plugins)&lt;/td&gt;
&lt;td&gt;Bot signups, fake accounts&lt;/td&gt;
&lt;td&gt;A Turnstile check or email verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login (&lt;code&gt;wp-login.php&lt;/code&gt;, WooCommerce My Account)&lt;/td&gt;
&lt;td&gt;Credential stuffing&lt;/td&gt;
&lt;td&gt;A short wait plus a challenge after failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contact and lead forms&lt;/td&gt;
&lt;td&gt;Spam leads&lt;/td&gt;
&lt;td&gt;A challenge for hosting networks only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WooCommerce checkout&lt;/td&gt;
&lt;td&gt;Card testing, fraud&lt;/td&gt;
&lt;td&gt;A challenge, or hold the order for review&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Risky" should mean several signals together (a datacenter IP plus a brand-new account plus a burst of attempts), not a VPN on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Your options on WordPress
&lt;/h2&gt;

&lt;p&gt;There are three common routes, and they combine well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare WAF rules.&lt;/strong&gt; If your site is behind Cloudflare, custom rules can block or challenge by ASN, country or Cloudflare's own threat signals, before the cache. Powerful, but you write and maintain the rules yourself, and you don't get a per-form view of what happened.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A blocker plugin backed by an IP API.&lt;/strong&gt; Plugins such as Proxy &amp;amp; VPN Blocker (proxycheck.io) or VPN Guard (vpnapi.io) check the visitor IP against an API and block or redirect on pages you choose. Simple and effective for hard blocking; mind the caching caveat above and the API's daily quota.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A monitoring-first plugin with form protection.&lt;/strong&gt; Score every visit and form submit, see the reasons in a dashboard, and add friction only on the forms and signals that turn out to matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where FindIP Shield fits
&lt;/h2&gt;

&lt;p&gt;Disclosure: we make FindIP Shield, a free plugin for the third route.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every visit, signup, login and WooCommerce checkout gets a risk score with the reasons: VPN, proxy, Tor, relay, hosting or datacenter network, or a known malicious IP.&lt;/li&gt;
&lt;li&gt;It runs on regular pages and on &lt;code&gt;wp-login.php&lt;/code&gt;, in the browser, so full-page caching doesn't skip it.&lt;/li&gt;
&lt;li&gt;With WooCommerce active it adds cart, checkout, payment-failure and order-completion context, so you can see which orders came through anonymizing networks.&lt;/li&gt;
&lt;li&gt;It doesn't block anything by default. When you're ready, you switch on form protection from the dashboard and choose what a risky visitor sees: a message, a short wait, or a Cloudflare Turnstile check.&lt;/li&gt;
&lt;li&gt;It never reads form values, passwords, emails or payment details, and starts in strict privacy mode.&lt;/li&gt;
&lt;li&gt;Setup is install, click &lt;strong&gt;Connect&lt;/strong&gt;, pick your site. Free up to 10,000 events per site per day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's on WordPress.org as &lt;a href="https://wordpress.org/plugins/findip-shield/" rel="noopener noreferrer"&gt;FindIP Shield: VPN, Proxy &amp;amp; Bot Traffic Detection&lt;/a&gt;, and there's an &lt;a href="https://www.findip.net/shield/demo" rel="noopener noreferrer"&gt;example dashboard with sample data&lt;/a&gt; if you want to see what it shows before installing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Find out which networks your visitors come from: residential, hosting, VPN, Tor.&lt;/li&gt;
&lt;li&gt;Monitor for a week before blocking anything.&lt;/li&gt;
&lt;li&gt;Remember that page caching skips server-side checks; check at the edge or in the browser.&lt;/li&gt;
&lt;li&gt;Add friction on the forms where abuse costs money, when several signals stack up.&lt;/li&gt;
&lt;li&gt;Never block VPN users outright. Challenge them when the rest of the picture looks wrong.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>wordpress</category>
      <category>security</category>
      <category>webdev</category>
      <category>woocommerce</category>
    </item>
    <item>
      <title>How to stop free-tier abuse on an AI app (without killing signups)</title>
      <dc:creator>FindIP</dc:creator>
      <pubDate>Thu, 22 Oct 2026 14:22:24 +0000</pubDate>
      <link>https://dev.to/findip/how-to-stop-free-tier-abuse-on-an-ai-app-without-killing-signups-nmg</link>
      <guid>https://dev.to/findip/how-to-stop-free-tier-abuse-on-an-ai-app-without-killing-signups-nmg</guid>
      <description>&lt;p&gt;On most SaaS products a fake free account costs you a row in a database. On an AI app it costs you tokens. Every new account that burns its free credits is a bill, and the people who farm free tiers know it: new email, new browser profile, sometimes a VPN, same workload as yesterday.&lt;/p&gt;

&lt;p&gt;The usual fixes each have a hole. Email verification stops nobody with a disposable inbox. A card on the free tier stops the abuse and most of your signups with it. IP rate limits lock out a whole office behind one NAT.&lt;/p&gt;

&lt;p&gt;What works is layers, each cheap on its own, ordered so the first ones protect your bill even before you catch anyone. This is the setup, in the order we'd build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Cap the blast radius first
&lt;/h2&gt;

&lt;p&gt;Before you try to detect anything, make sure the worst case is a number you chose.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A global daily budget for all free usage.&lt;/strong&gt; When it's spent, free requests queue or wait until tomorrow. One bad night can't become a surprise invoice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A per-account daily cap&lt;/strong&gt; that's lower than you think you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check both on the server, before the model call&lt;/strong&gt;, not after.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before calling the model for a free-tier user&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;canSpendFree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;estTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;global&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;incrby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`free:global:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;estTokens&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;incrby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`free:user:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;estTokens&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`free:global:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;172800&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`free:user:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;172800&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;GLOBAL_FREE_DAILY&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;USER_FREE_DAILY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't stop farming. It makes farming boring and bounded, which is most of the battle.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make each account worth less
&lt;/h2&gt;

&lt;p&gt;Farming is economics. Lower the payout per account and most farmers stop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drip credits instead of one lump.&lt;/strong&gt; "50 a day for a week" is far less worth farming than "350 now". An honest user barely notices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start new accounts on a cheaper model.&lt;/strong&gt; The good model unlocks after email verification and a bit of normal use. The farmer's cost per account goes up; yours goes down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unlock more with trust&lt;/strong&gt;: account age, normal usage, a verified email, or a card, but ask for the card only when an account looks risky (step 5).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Clean up the email layer
&lt;/h2&gt;

&lt;p&gt;Not because it stops determined people, but because it's an afternoon of work and removes the laziest farming.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DISPOSABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;disposableDomains&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// e.g. the open-source "disposable-email-domains" list&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;googlemail.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gmail.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;                 &lt;span class="c1"&gt;// drop +tags&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gmail.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Gmail ignores dots&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;disposable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DISPOSABLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the normalized email with a unique index, and keep a hash of it when an account is deleted, so "delete and sign up again" doesn't reset the free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Group accounts instead of judging each one
&lt;/h2&gt;

&lt;p&gt;A single signal proves nothing. A VPN is common for privacy-minded users; a shared IP is every office and mobile carrier. What gives farmers away is that their accounts &lt;strong&gt;look alike&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network type, not just IP.&lt;/strong&gt; An office NAT or a mobile carrier is a residential or mobile ISP. Ten accounts in an hour from VPN exits and cloud servers are not. This distinction is what keeps your false positives down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network changes inside one session&lt;/strong&gt;: the IP, ASN or country switching mid-signup is a strong sign of a rotating proxy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timing&lt;/strong&gt;: a new signup minutes after the previous account ran out of credits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workload&lt;/strong&gt;: the same first prompts, files or project shape as an older account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Log these for every signup from day one. You can't cluster history you didn't keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Add friction only where signals stack up
&lt;/h2&gt;

&lt;p&gt;Now use the signals, gently:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signals&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nothing unusual&lt;/td&gt;
&lt;td&gt;Straight in, normal free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One weak signal (a VPN, say)&lt;/td&gt;
&lt;td&gt;Straight in, maybe the drip starts smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Several signals, or a known-bad network&lt;/td&gt;
&lt;td&gt;A Turnstile check, email or phone verification before credits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clearly the same person as a capped account&lt;/td&gt;
&lt;td&gt;No free credits until they verify with a card&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The honest majority never sees any of this. The farmer sees it on every account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where FindIP Shield fits
&lt;/h2&gt;

&lt;p&gt;Disclosure: we make FindIP Shield, a free tool that does the network part of steps 4 and 5. It scores each visit and form submit from 0 to 100 with the reasons (VPN, proxy, Tor, hosting or datacenter network, known malicious IP, IP changes during the session), and can add a short wait, a Turnstile check or a stop message for risky submits only, off until you switch it on. It scores the network, not the device: there's no device fingerprinting, so on its own it won't recognize the same laptop on a fresh profile. It never reads form values.&lt;/p&gt;

&lt;p&gt;The browser side is one script tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script
  &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.findip.net/shield/v1.js"&lt;/span&gt;
  &lt;span class="na"&gt;data-site-key=&lt;/span&gt;&lt;span class="s"&gt;"pub_xxxxxxxxx"&lt;/span&gt;
  &lt;span class="na"&gt;data-auto-track=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
  &lt;span class="na"&gt;data-privacy-mode=&lt;/span&gt;&lt;span class="s"&gt;"balanced"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send the Shield session ID with your signup request (&lt;code&gt;FindIP.getSession().sessionId&lt;/code&gt;), then let your server make the decision, because anything decided in the browser can be bypassed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shieldVerdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://shield.findip.net/v1/shield/sessions/verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SHIELD_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// sec_..., server only&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup_attempt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Unknown is not safe: no session, or no IP intelligence for it.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;risk_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monitor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recommendation&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;require_card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recommendation&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;challenge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challenge_passed&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;require_verification&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;allow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combine that verdict with your own signals from steps 3 and 4, and feed it into the table in step 5. Shield is free up to 10,000 events per site per day; there's also a WordPress plugin, a Shopify app, a GTM template and a no-code Cloudflare integration if your signup page isn't hand-written.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Cap global and per-account free spend on the server.&lt;/li&gt;
&lt;li&gt;Drip credits and start new accounts on a cheaper model.&lt;/li&gt;
&lt;li&gt;Normalize emails, block disposable domains, remember deleted accounts.&lt;/li&gt;
&lt;li&gt;Cluster accounts by network type, timing and workload.&lt;/li&gt;
&lt;li&gt;Add friction only where signals stack up, and never block VPN users outright.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You won't stop every determined person. You don't need to: you need farming to cost them more than it saves, while honest users still get in with one click.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>We made visitor risk scoring free. Here's how to add it to signup and checkout</title>
      <dc:creator>FindIP</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:22:14 +0000</pubDate>
      <link>https://dev.to/findip/we-made-visitor-risk-scoring-free-heres-how-to-add-it-to-signup-and-checkout-2216</link>
      <guid>https://dev.to/findip/we-made-visitor-risk-scoring-free-heres-how-to-add-it-to-signup-and-checkout-2216</guid>
      <description>&lt;p&gt;Fake signups and trial abuse cost money before anyone pays you: compute, emails, support time. The usual fixes are a CAPTCHA on every visitor, which costs you real signups, or a fraud vendor that bills per lookup.&lt;/p&gt;

&lt;p&gt;We are the FindIP team, a small group of developers. For years, developers have used our IP intelligence API to find out where a connection really comes from: its location, and whether it is a VPN, a proxy, Tor or a datacenter. In all that time we watched them build the same thing on top of it, again and again: look up the IP on signup, keep some state, decide whether to add friction. So we built it properly, once, and made it free. It is called &lt;strong&gt;FindIP Shield&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.findip.net/shield/demo?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=shield_oct_2026&amp;amp;utm_content=dev_article_intro_demo" rel="noopener noreferrer"&gt;See your own connection scored in the live demo&lt;/a&gt;&lt;/strong&gt;. No signup needed.&lt;/p&gt;

&lt;p&gt;What you get, free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 0 to 100 risk score with reasons&lt;/strong&gt; on every signup, login, lead and checkout attempt: VPN, proxy, Tor, hosting network, known-malicious address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotation detection.&lt;/strong&gt; A session whose IP, network or country changes midway is flagged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A response per form that you choose:&lt;/strong&gt; watch only, slow down, a Turnstile check, or stop. Installing changes nothing until you pick one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No metering.&lt;/strong&gt; API calls are unlimited under fair usage, and each site records up to 10,000 events a day. No credit card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy by design.&lt;/strong&gt; The SDK is MIT licensed and never reads what a visitor types.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post shows how to add it in four steps. The pattern works without Shield too, so if you take one thing away, take the rule below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; a browser-side risk result is for visibility and user experience. If a result is going to deny access, stop a payment or add real friction, verify the session on your server first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The case we are designing for
&lt;/h2&gt;

&lt;p&gt;A product offers a free trial and a discounted first payment. Someone creates several accounts with different emails. The per-IP limit catches the first burst. Later attempts arrive through different proxies and hosting providers, one or two per address, and look like ordinary signups.&lt;/p&gt;

&lt;p&gt;No single detail proves anything. The useful part is the combination: repeated signup events, changing infrastructure, and elevated network risk around the same valuable action. The application needs three answers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What action just happened?&lt;/li&gt;
&lt;li&gt;What explains the risk result?&lt;/li&gt;
&lt;li&gt;What is the least disruptive response that still protects the product?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 1: report the action, not the person
&lt;/h2&gt;

&lt;p&gt;Install the SDK once near the browser entry point. The script-tag form is the same idea for sites without a bundler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @findip/shield
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getSession&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@findip/shield&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;siteKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pub_xxxxxxxxx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;privacyMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;balanced&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoTrack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoDetectForms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK recognises common signup, login, lead and checkout forms from their attributes, and reports the submit itself as &lt;code&gt;signup_attempt&lt;/code&gt;, &lt;code&gt;login_attempt&lt;/code&gt; and so on. Do not send the same attempt again from your own code, or it is counted twice. Use &lt;code&gt;track()&lt;/code&gt; for what the form cannot know: the outcome, or an attempt that fires no form &lt;code&gt;submit&lt;/code&gt; event, such as a request sent from a click handler.&lt;/p&gt;

&lt;p&gt;Keep the context small and business-shaped. A plan name or an email domain explains an event; a raw email, a password or a card number has no place in the payload.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Form detection already sent signup_attempt. Report the outcome.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup_success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;email_domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// No result came back. This is "we do not know", not "safe".&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checkout follows the same rule: &lt;code&gt;transaction_amount&lt;/code&gt; and &lt;code&gt;currency&lt;/code&gt; are context; payment details are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: decide what each result means before launch
&lt;/h2&gt;

&lt;p&gt;The result carries a score when intelligence is available, a recommendation and readable reasons (proxy, Tor, hosting network, a rotating IP inside the session, a malicious-IP category). Decide the response table now, not after the first suspicious checkout:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;th&gt;Reasonable response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Continue. Rate limits and account checks still apply.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;monitor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Continue, keep the event for correlation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;challenge&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Something proportionate: email confirmation, MFA, a Turnstile check.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only under a documented server-side policy, with a recovery path.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No result, or &lt;code&gt;unknown&lt;/code&gt; status from the server check&lt;/td&gt;
&lt;td&gt;Intelligence was unavailable. Not a score of zero.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mistake that makes risk scoring hateful is turning one signal into a hard rule. A VPN flag is not proof of fraud. Corporate networks, privacy-conscious customers, mobile carriers and travellers all show up with signals that look odd in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: do not make the browser a trust boundary
&lt;/h2&gt;

&lt;p&gt;Anyone who can open developer tools can rewrite browser code or fabricate a result. So the browser never decides. It passes the session ID along with the business request, and the server verifies it with a secret key that only the server holds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Browser: attach the session to the signup request.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/signup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;findip_session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Server: verify before enforcing anything.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://shield.findip.net/v1/shield/sessions/verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FINDIP_SHIELD_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findip_session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signup_attempt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;risk_status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;continueWithoutRiskBasedEnforcement&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recommendation&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;block&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;rejectUnderDocumentedPolicy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recommendation&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;challenge&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;challenge_passed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;requireAdditionalVerification&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verified response also describes the whole session: distinct IPs and networks seen, whether the IP or ASN rotated, and network flags that were true for any event. That is the record you want in a fraud queue or an order note, and it comes from the server, not from the page.&lt;/p&gt;

&lt;p&gt;For plain HTML forms, &lt;code&gt;sessionField: true&lt;/code&gt; makes the SDK add the session ID as a hidden field, so the server does not depend on a cookie that a privacy-minded browser may refuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: choose a privacy mode on purpose
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Fits&lt;/th&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;strict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Consent denied, or minimal collection&lt;/td&gt;
&lt;td&gt;Session-only identity, no visitor cookie, user agent, language and time zone only. Form metadata, never values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;balanced&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A sensible default&lt;/td&gt;
&lt;td&gt;Adds a 30-day first-party visitor ID, screen and device details, and an optional picture of a form with every value blanked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;advanced&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Longer retention&lt;/td&gt;
&lt;td&gt;The same fields as &lt;code&gt;balanced&lt;/code&gt;, marked for extended retention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every mode excludes what a visitor types: passwords, card numbers, emails, phone numbers and every other field value. No keystrokes, no page HTML, no screenshot of the page and no session recording. The one picture is the blanked form in &lt;code&gt;balanced&lt;/code&gt; and &lt;code&gt;advanced&lt;/code&gt;, and &lt;code&gt;captureFormSnapshots: false&lt;/code&gt; turns it off. On ingestion the event is rebuilt from an allowlist rather than stored as whatever the browser sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would not do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Switch on automatic blocking on day one. Watch ordinary traffic first, long enough to learn what your employees, your corporate customers and your mobile users look like.&lt;/li&gt;
&lt;li&gt;Treat an unavailable lookup as a score of zero. Handle &lt;code&gt;unknown&lt;/code&gt; explicitly on both sides.&lt;/li&gt;
&lt;li&gt;Reject a payment on a browser-side score. Verify, then decide, and give a legitimate user a way to recover when a challenge fails.&lt;/li&gt;
&lt;li&gt;Collect "just the email" for convenience. Hash it in the browser if you need to tie a session to an account; the SDK does that for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Before adding friction
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the allowed domain is right and the first event reaches the dashboard.&lt;/li&gt;
&lt;li&gt;Run in visibility mode long enough to understand normal traffic.&lt;/li&gt;
&lt;li&gt;Use the lowest privacy mode that gives the context you actually need.&lt;/li&gt;
&lt;li&gt;Handle &lt;code&gt;unknown&lt;/code&gt; explicitly in browser and server code.&lt;/li&gt;
&lt;li&gt;Verify any decision that stops a signup, payment or account action.&lt;/li&gt;
&lt;li&gt;Give legitimate users a recovery path when a challenge fails.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;See it first:&lt;/strong&gt; the &lt;a href="https://www.findip.net/shield/demo?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=shield_oct_2026&amp;amp;utm_content=dev_article_demo" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; scores your own connection and shows a sample dashboard. No signup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install it:&lt;/strong&gt; the &lt;a href="https://www.findip.net/docs/shield/quickstart?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=shield_oct_2026&amp;amp;utm_content=dev_article_quickstart" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; covers npm, a script tag, WordPress, Shopify, Google Tag Manager and a no-code Cloudflare Worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the code:&lt;/strong&gt; the SDK is on &lt;a href="https://github.com/findip-net/findip-shield-js" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; under the MIT licence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We would like to hear what would make this more useful to you. Questions about the trust model are welcome in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: we are the FindIP team, and Shield is our product. It is free in its current preview. The full guide is the canonical version at &lt;a href="https://www.findip.net/Docs/visitor-risk-scoring-signup-checkout?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=shield_oct_2026&amp;amp;utm_content=dev_article_canonical" rel="noopener noreferrer"&gt;findip.net&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>security</category>
      <category>webdev</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
