From FindIP Engineering

Adding visitor risk scoring to signup and checkout

A practical way to add useful network context without sending sensitive form data or trusting a decision made in the browser.

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.

One rule before we write code

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:

  1. What action just happened?
  2. What explains the risk result?
  3. What is the least disruptive response that still protects the product?
Signup attemptThe browser reports the event, not field values.
Risk enrichmentShield adds IP, network, reputation, and rotation signals.
Server verificationYour backend confirms the session using the secret key.
Proportionate actionAllow, monitor, challenge, or block under your policy.

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.

RecommendationReasonable application response
allowContinue normally. Existing rate limits and account checks still apply.
monitorLet the action continue and keep the event for correlation or review.
challengeAsk for something proportionate: email confirmation, MFA, or another verification step.
blockUse only when a documented server-side policy supports it and the user has a recovery path.
unknown statusThe 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();
}
The sec_ key is server-only

Do 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

ModeBest fitMain behavior
strictConsent denied or minimal collectionSession-only identity, minimal browser context, and no persistent visitor cookie or localStorage.
balancedA sensible starting point for most sitesFirst-party session context, optional visitor continuity, page and campaign context, and form metadata, never values.
advancedAn approved repeat-visitor use caseLonger 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 unknown explicitly 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.

Start with Shield free Open the quickstart View on npm