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.
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 FindIP Shield.
See your own connection scored in the live demo. No signup needed.
What you get, free:
- A 0 to 100 risk score with reasons on every signup, login, lead and checkout attempt: VPN, proxy, Tor, hosting network, known-malicious address.
- Rotation detection. A session whose IP, network or country changes midway is flagged.
- A response per form that you choose: watch only, slow down, a Turnstile check, or stop. Installing changes nothing until you pick one.
- No metering. API calls are unlimited under fair usage, and each site records up to 10,000 events a day. No credit card.
- Privacy by design. The SDK is MIT licensed and never reads what a visitor types.
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.
The rule: 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.
The case we are designing for
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.
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:
- What action just happened?
- What explains the risk result?
- What is the least disruptive response that still protects the product?
Step 1: report the action, not the person
Install the SDK once near the browser entry point. The script-tag form is the same idea for sites without a bundler.
npm install @findip/shield
import { init, track, getSession } from '@findip/shield';
init({
siteKey: 'pub_xxxxxxxxx',
privacyMode: 'balanced',
autoTrack: true,
autoDetectForms: true,
});
The SDK recognises common signup, login, lead and checkout forms from their attributes, and reports the submit itself as signup_attempt, login_attempt and so on. Do not send the same attempt again from your own code, or it is counted twice. Use track() for what the form cannot know: the outcome, or an attempt that fires no form submit event, such as a request sent from a click handler.
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.
// Form detection already sent signup_attempt. Report the outcome.
const result = await track('signup_success', {
email_domain: 'example.com',
plan: 'free',
});
if (!result?.risk) {
// No result came back. This is "we do not know", not "safe".
}
Checkout follows the same rule: transaction_amount and currency are context; payment details are not.
Step 2: decide what each result means before launch
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:
| Recommendation | Reasonable response |
|---|---|
allow |
Continue. Rate limits and account checks still apply. |
monitor |
Continue, keep the event for correlation. |
challenge |
Something proportionate: email confirmation, MFA, a Turnstile check. |
block |
Only under a documented server-side policy, with a recovery path. |
No result, or unknown status from the server check |
Intelligence was unavailable. Not a score of zero. |
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.
Step 3: do not make the browser a trust boundary
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.
// Browser: attach the session to the signup request.
const { sessionId } = getSession();
await fetch('/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, findip_session_id: sessionId }),
});
// Server: verify before enforcing anything.
const response = await fetch('https://shield.findip.net/v1/shield/sessions/verify', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FINDIP_SHIELD_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ session_id: req.body.findip_session_id, event: 'signup_attempt' }),
});
const verified = await response.json();
if (!verified.verified || verified.risk_status === 'unknown') {
return continueWithoutRiskBasedEnforcement();
}
if (verified.recommendation === 'block') {
return rejectUnderDocumentedPolicy();
}
if (verified.recommendation === 'challenge' && !verified.challenge_passed) {
return requireAdditionalVerification();
}
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.
For plain HTML forms, sessionField: true 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.
Step 4: choose a privacy mode on purpose
| Mode | Fits | Behaviour |
|---|---|---|
strict |
Consent denied, or minimal collection | Session-only identity, no visitor cookie, user agent, language and time zone only. Form metadata, never values |
balanced |
A sensible default | Adds a 30-day first-party visitor ID, screen and device details, and an optional picture of a form with every value blanked |
advanced |
Longer retention | The same fields as balanced, marked for extended retention |
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 balanced and advanced, and captureFormSnapshots: false turns it off. On ingestion the event is rebuilt from an allowlist rather than stored as whatever the browser sent.
What we would not do
- 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.
- Treat an unavailable lookup as a score of zero. Handle
unknownexplicitly on both sides. - Reject a payment on a browser-side score. Verify, then decide, and give a legitimate user a way to recover when a challenge fails.
- 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.
Before adding friction
- Confirm the allowed domain is right and the first event reaches the dashboard.
- Run in visibility mode long enough to understand normal traffic.
- Use the lowest privacy mode that gives the context you actually need.
- Handle
unknownexplicitly in browser and server code. - Verify any decision that stops a signup, payment or account action.
- Give legitimate users a recovery path when a challenge fails.
Try it
- See it first: the live demo scores your own connection and shows a sample dashboard. No signup.
- Install it: the quickstart covers npm, a script tag, WordPress, Shopify, Google Tag Manager and a no-code Cloudflare Worker.
- Read the code: the SDK is on GitHub under the MIT licence.
We would like to hear what would make this more useful to you. Questions about the trust model are welcome in the comments.
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 findip.net.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more