Skip to main content
Regional pricing rewards buyers in lower-cost markets with a cheaper rate. The catch is that anyone can sit behind a VPN, a proxy, or iCloud Private Relay, point their session at a discount region, and claim the lower price from anywhere. This pattern reads the network country and the anonymity signals on the visit, then lets your pricing code decide whether the claimed region is trustworthy enough to honor.

What is regional pricing abuse?

Regional pricing abuse is when a buyer fakes their location, usually with a VPN, proxy, or relay that exits in a low-cost country, to claim a discounted price they are not eligible for. The geolocation looks local, but the network is masking where the person actually sits and pays.

How ShieldLabs surfaces it

ShieldLabs returns two two-letter ISO countries plus the anonymity signals on the visit, so your code can tell a real local buyer from a masked one:
  • public_ip.country — resolved from the public IP, which a VPN can set to any region.
  • local_ip.country — resolved from the real network IP, which can reveal the underlying network behind that VPN. Empty when the follow-up network check could not complete; treat absent real-network data as unverified, not clean.
When the two disagree, detection_flags.ip_mismatch becomes true — a strong sign the visible exit country is not the buyer’s real one. ShieldLabs surfaces this comparison for your code; it is informational and does not by itself change the Risk Score (0–100). So a public-IP country check by itself is easy to defeat; compare the claimed region against local_ip.country when it is present, falling back to public_ip.country. The durable DeviceID survives cleared cookies, incognito, and IP rotation, so a repeat region-shopper who re-rolls the session resolves to the same device. ShieldLabs surfaces the countries and the signals; your code owns the price.

Prevent region-shopping

Read both countries, the ip_mismatch flag, the anonymity signals, and the durable DeviceID when the buyer confirms a region. The rule your code applies: honor the discount only when the network country matches the claimed region and no masking signal fires; otherwise — if the two countries disagree (ip_mismatch), the session carries a VPN, Proxy, Tor, Privacy Relay, Datacenter IP, or Browser VPN/Proxy signal, or the exit country does not match the claim — fall back to the standard price or ask for a billing-country check. You gate a price, not a person.

Build it

1

Create a ShieldLabs account and get your keys

Sign up for free and get 5,000 identifications, or log in if you already have an account. Register the domain you want to identify visitors on, then open the Keys page. Use the Public Key to initialize the snippet in the browser, and keep your server-side credentials on your backend: the Private API Key authenticates the History API, and each webhook endpoint has its own whsec_… signing secret. See Keys.
2

Identify when the region is set

Load the snippet on the checkout or plan-selection page. When the buyer selects or confirms a region, force a current read with the forceCheck* variant: it clears the session and runs a new identify call immediately, so the country and signals reflect the session at decision time. Pass a hashed or pseudonymous user id to the authenticated call, never a raw email or account id.
pricing.html
<script type="module">
  const mod = await import(
    'https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY'
  );
  // The browser does NOT compute the Risk Score or resolve the country.
  // The callback's first arg is the client IP the server saw (not a score);
  // the requestID is your join key to the server webhook.
  document.getElementById('region-select').addEventListener('change', (e) => {
    mod.forceCheckAuthenticatedUser('a1b2c3d4hasheduserid', (ip, requestID) => {
      document.getElementById('shield-request-id').value = requestID;
      document.getElementById('claimed-region').value = e.target.value; // e.g. "BR"
    });
  });
</script>

<form id="checkout-form" method="POST" action="/api/price">
  <input type="hidden" id="shield-request-id" name="shieldRequestId" />
  <input type="hidden" id="claimed-region" name="claimedRegion" />
  <button type="submit">Continue</button>
</form>
3

Receive the webhook and cache both countries

Within about 1 second of the check, ShieldLabs POSTs a signed risk event to your endpoint. Verify X-Shield-Signature on the raw body, then cache the result keyed by request_id — the shared waitForScore helper does this and returns the raw webhook payload, so public_ip, local_ip, and detection_flags are all available to the pricing handler below without any extra mapping.A geo-pricing body where the buyer claims a Brazil price, the public IP exits in BR, but the real network IP puts the underlying network in the US:
{
  "request_id": "8f1d0c2a-7b3e-4a9c-9d2f-1e6a5b4c3d21",
  "device_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "public_ip": { "ip": "203.0.113.42",  "country": "BR" },
  "local_ip": { "ip": "198.51.100.23", "country": "US" },
  "risk_score": 15,
        "signals": [
    { "name": "VPN", "weight": 15 }
  ],
  "detection_flags": { "vpn": true, "ip_mismatch": true },
  "observed_at": "2026-06-16T10:00:00Z"
}
The two countries disagree, so ip_mismatch is true, and a VPN signal is present: the claimed region is masked. Gate on the country comparison and the flag, not on the score alone.
4

Gate the price on country and anonymity

Wait briefly for the score, then weigh four things: is the session anonymized, do the two countries disagree (ip_mismatch), does the network country match the claimed region, and how strong is the Score. Any anonymity signal, a country mismatch, or ip_mismatch is enough to stop honoring the discount. Branch on the detection_flags booleans, not the label text, which can change.
price.js
app.post('/api/price', async (req, res) => {
  const { shieldRequestId, claimedRegion, userId } = req.body;

  // Wait up to ~2s for the webhook; falls back to the History API.
  const shield = await waitForScore(shieldRequestId, 2000);

  // No result yet is not "verified". Default to the standard price rather
  // than handing out a discount on missing data.
  if (!shield) {
    return res.json({ price: standardPrice(userId), region: 'standard', reason: 'verifying' });
  }

  const f = shield.detection_flags ?? {}; // detection_flags booleans, the stable contract

  // Network-level masking that makes the exit country untrustworthy.
  const anonymized =
    f.vpn || f.proxy || f.tor || f.private_relay || f.datacenter_ip || f.browser_vpn_proxy;

  // Compare the claimed region to the real-network country when captured;
  // otherwise fall back to the public-IP country.
  const realCountry = shield.local_ip?.country || shield.public_ip?.country;
  const countryMatches = realCountry === claimedRegion;

  // Masked session: the claimed region cannot be trusted.
  if (anonymized) {
    return res.json({
      price: standardPrice(userId),
      region: 'standard',
      reason: 'region_unverified_anonymized',
    });
  }

  // Public IP and real internal IP resolve to different networks, or the
  // exit country does not match the claim.
  if (f.ip_mismatch || !countryMatches) {
    return res.json({
      price: standardPrice(userId),
      region: 'standard',
      reason: f.ip_mismatch ? 'region_ip_mismatch' : 'region_mismatch',
      verify: true,
    });
  }

  // Countries line up and the session is not masked: honor the regional price.
  return res.json({ price: regionalPrice(claimedRegion), region: claimedRegion });
});
The fallback, hold, and verification steps run in your application. ShieldLabs returns the two countries, the score, the signals, and the detection_flags; your pricing code owns the decision.

Reading the signals for a price decision

For a checkout decision you weigh the Score and its bands. For a regional-price claim the country comparison carries most of the weight, because a masked session can score Low yet still be hiding its location. The four bands are defined in Risk Scoring; here is how to read the inputs together.
Country vs claimed regionAnonymity signal in signalsReasonable action
MatchNoneHonor the regional price
MatchVPN / Proxy / Privacy Relay presentVerify before discount; a corporate VPN can match by coincidence
ip_mismatch flag set (countries disagree)AnyStandard price, ask for verification
Mismatch (public_ip.country ≠ claim)NoneStandard price, ask for verification
MismatchAny presentStandard price, or hold for review
Unknown (no webhook yet)UnknownStandard price until verified
The names below are the human-readable labels in signals for visibility and logging; branch on the matching detection_flags boolean, not the label text. Each adds to the Score by the weight shown; the exact weights live in the Risk Scoring table.
Signal in signalsWeightWhy it breaks a region claim
Tor99Connection exits through the Tor network, so the country is the exit node, not the buyer.
Browser VPN/Proxy30An in-browser VPN or proxy extension routes the session, so the exit country was toggled, not lived in.
VPN15Traffic routes through a VPN, so the exit country is chosen, not where the buyer sits.
Privacy Relay15iCloud Private Relay relays the connection, so the visible country can differ from the real one.
Proxy10IP flagged as a proxy. The geolocated country reflects the proxy, not the person.
Datacenter IP10IP is in a hosting range. A real shopper on a personal device rarely exits from a datacenter.
Timezone Mismatch10The browser timezone disagrees with the IP timezone, a corroborating tell that the geolocated country is not where the device actually is.
A legitimate buyer can trip this. A traveler abroad, a corporate VPN, or a privacy browser all detach the network country from where the customer actually lives and pays. This is why the play gates a price decision, not a ban: fall back to the standard price, ask for a billing-country or payment check, or hold for review. Decide on the two countries, the anonymity signals, and your own verification step together, never on one input alone, and let the buyer prove their region rather than locking them out.

Test it

Open your pricing page behind a VPN whose exit is in a discount region, then select that region. The webhook should carry the masking signal in signals and an public_ip.country that does not match the claim, so your code falls back to the standard price. Now clear cookies, reopen the page in incognito, or switch to a second browser on the same machine — the durable DeviceID returns the same, so a buyer re-rolling the session to retry the discount still resolves to one device. Across visits, a signed-in account that claims several discount regions in a day also surfaces on the dashboard as the Multiple Countries on One Account pattern.

Next steps

Signals

Every anonymity signal that can appear in signals, in plain language, with its weight.

The Risk Score

How the 0 to 100 score is built, what the signals array carries, and the band definitions.

Checkout protection

The fresh-check pattern at the payment step, where the same signals gate the charge.

Acting on the Risk Score

Turn the Score, the country fields, and the signals into allow, verify, and hold logic in your app.