Rate limiting is usually the first control added to a public signup form. It is simple, cheap, and worth having. It also stops being very informative as soon as someone spreads requests across several addresses.
That is where visitor risk scoring can help. It adds context about the connection behind an action: proxy or Tor use, hosting infrastructure, network reputation, and unusual session changes. The score is not a verdict, though. We built FindIP Shield to provide another signal your application can use, not an excuse to hand control of signup or payment decisions to browser JavaScript.
A browser-side result is useful for visibility and user experience. If a result will deny access, stop a payment, or create meaningful friction, verify the session on your server first.
The failure case we are designing for
Consider a product that offers a free trial and a discounted first payment. Someone creates several accounts, changing email addresses each time. A per-IP limit catches the first burst, but later attempts arrive through different proxy addresses and hosting providers.
No single detail proves abuse. The useful part is the combination: repeated signup events, changing infrastructure, and elevated network risk around the same valuable action. The application needs to answer three questions:
- What action just happened?
- What explains the risk result?
- What is the least disruptive response that still protects the product?
Start with the browser signal
After creating a Shield site for the domain, install the public package:
npm install @findip/shield
Initialize it once near the browser entry point. We recommend starting with balanced. It gives an investigation enough context to be useful without jumping straight to the longest visitor continuity available.
import { init, track, getSession } from '@findip/shield';
init({
siteKey: 'pub_xxxxxxxxx',
privacyMode: 'balanced',
autoTrack: true,
autoDetectForms: true,
});
For a site without a bundler, the versioned script is the same idea:
<script
src="https://cdn.findip.net/shield/v1.js"
data-site-key="pub_xxxxxxxxx"
data-auto-track="true"
data-privacy-mode="balanced">
</script>
Name the action, not the person
Shield can recognize common signup, login, lead, and checkout forms automatically. We still prefer explicit events for important flows because the event name becomes part of the investigation. Keep the context small. A plan name or email domain may explain the event; a raw email address, password, card number, or full form body does not belong in the payload.
const signupRisk = await track('signup_attempt', {
email_domain: 'example.com',
plan: 'free',
});
if (signupRisk.risk.status === 'unknown') {
// Intelligence was unavailable. Do not treat this visitor as safe.
showNormalSignupFlow();
} else if (signupRisk.risk.recommendation === 'challenge') {
showAdditionalVerification();
}
The checkout event follows the same rule. Business context is useful; payment credentials are not:
await track('checkout_started', {
plan: 'pro',
currency: 'USD',
transaction_amount: 49,
});
Decide what the result means before launch
The response includes a status, a score when intelligence is available, a recommendation, and readable reasons. It can also identify VPN, proxy, Tor, relay, hosting, or known malicious infrastructure. Do not wait until the first suspicious checkout to decide how each result should behave.
| Recommendation | Reasonable application response |
|---|---|
| allow | Continue normally. Existing rate limits and account checks still apply. |
| monitor | Let the action continue and keep the event for correlation or review. |
| challenge | Ask for something proportionate: email confirmation, MFA, or another verification step. |
| block | Use only when a documented server-side policy supports it and the user has a recovery path. |
| unknown status | The lookup was unavailable. That means “we do not know,” not “safe.” |
Do not make the browser a trust boundary
Anyone who can open developer tools can change browser code or manufacture a response. Send the Shield session ID alongside the signup or checkout request, then verify it from the backend with the site's secret key.
// Browser: associate the business request with the Shield session.
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 a security decision.
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 === 'challenge') {
return requireAdditionalVerification();
}
sec_ key is server-onlyDo not put it in browser JavaScript, HTML, client-visible logs, analytics properties, or a public repository. The public pub_ site key is the only key intended for the page.
Choose privacy mode on purpose
| Mode | Best fit | Main behavior |
|---|---|---|
| strict | Consent denied or minimal collection | Session-only identity, minimal browser context, and no persistent visitor cookie or localStorage. |
| balanced | A sensible starting point for most sites | First-party session context, optional visitor continuity, page and campaign context, and form metadata, never values. |
| advanced | An approved repeat-visitor use case | Longer continuity and additional consistency signals, while the sensitive-data exclusions remain in place. |
Every mode excludes passwords, card numbers, raw email addresses and phone numbers, keystrokes, screenshots, DOM snapshots, and complete form values. On ingestion, Shield rebuilds the event from an allowlist instead of storing arbitrary browser payloads.
What we would avoid
The easiest way to make risk scoring frustrating is to turn a single signal into a hard rule. A VPN flag alone is not proof of fraud. A high score without server verification is not a safe reason to reject a payment. And an unavailable lookup must not quietly become a score of zero.
We would also avoid switching on automatic blocking on day one. Watch ordinary traffic first. You need to see how employees, privacy-conscious customers, corporate networks, mobile carriers, and legitimate travelers appear before choosing where extra friction belongs.
Before adding friction
- Confirm that the allowed domain is correct and the first event reaches the dashboard.
- Run in visibility mode long enough to understand normal traffic.
- Use the lowest privacy mode that provides the context you actually need.
- Handle
unknownexplicitly in both browser and server code. - Verify any decision that stops a signup, payment, or account action.
- Give legitimate users a way to recover when a challenge fails.
Start by observing one real flow
Add Shield to a signup or checkout page, confirm the event arrives, and look at the reasons behind the result. Enforcement can wait until the traffic makes sense.