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. ShieldLabs scores. Your code decides. ShieldLabs never picks a price or rejects a buyer. It returns a Risk Score (0 to 100), the anonymity signals behind it, and the two-letter ISO Country derived from the IP. Whether to show the regional price, ask for verification, or fall back to the standard price is logic you write in your checkout flow.

The pattern

1

Identify when the region is set

Load the snippet on the checkout or plan-selection page. When the buyer selects or confirms a region, call forceCheckAuthenticatedUser so you score the session as it is at the moment of the price decision, not a stale read from page load.
2

Receive the score, country, and signals

Correlate the browser requestID with the webhook your server receives, with a short timeout fallback to the History API. The body carries Country, the Score, and the Details.
3

Compare the claimed region to the evidence

If the session carries anonymity signals, or the network Country does not match the region the buyer claims, treat the claimed region as unverified.
4

Act in your own code

Show the real-region price, ask for verification, or hold for review. You gate a price, not a person, and you own the decision.

Step 1: identify when the region is set

On a pricing page you may already run checkAnonymous for analytics. When the buyer selects a region or reaches checkout with a regional price applied, 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. Always 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'
  );

  // Run a fresh check when the buyer confirms a region and its price.
  // The browser does NOT compute the Risk Score or resolve the country.
  // The callback hands you the requestID, your join key to the server webhook.
  document.getElementById('region-select').addEventListener('change', (e) => {
    mod.forceCheckAuthenticatedUser('a1b2c3d4hasheduserid', (serverResponse, 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>

Step 2: receive the webhook and read Country plus signals

Within about 1 second of the check, ShieldLabs POSTs a { Data, Assing } envelope to your callback URL. Verify the Assing HMAC first, then cache the result keyed by RequestID so the price request can look it up. This is the same verify, respond fast, store, expire handler every recipe shares, so the Cookbook defines it once as the scoreCache and waitForScore helper. That helper already stores Country alongside the score, so nothing extra is needed here. A geo-pricing body looks like this. The Country field is the two-letter ISO code resolved from the IP, and the anonymity signals ride in Details:
{
  "Data": {
    "RequestID": "8f1d0c2a-7b3e-4a9c-9d2f-1e6a5b4c3d21",
    "DeviceID": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "IP": "203.0.113.42",
    "Country": "US",
    "Score": 15,
    "Details": [
      { "Value": 15, "Description": "VPN" }
    ],
    "Phase": "initial"
  },
  "Assing": "1f3c9a...hex-hmac-sha256"
}
Here Country resolves to US while a VPN signal is present, so a session claiming a Brazil price is masking its location. The Risk Score on its own is only Low (15), which is exactly why the country mismatch matters: a low score is not a clean signal for a regional-price claim.
Description is a human-readable label for visibility and logging. Branch on whether an anonymity signal is present in Details and on the Country value, not on the exact label text, which can change.

Step 3: gate the price on country and anonymity

Wait briefly for the score, then compare three things: is the session anonymized, does the network country match the claimed region, and how strong is the Score. The presence of any anonymity signal, or a country that does not line up with the claim, is enough to stop honoring the discount and fall back to your standard price.
price.js
// Anonymizing signals that make a claimed region unverifiable. Match on
// presence in Details, never on exact label text, which can change.
const ANON_SIGNALS = ['vpn', 'proxy', 'privacy relay', 'datacenter', 'tor'];

function isAnonymized(details) {
  return details.some((d) =>
    ANON_SIGNALS.some((s) => d.Description.toLowerCase().includes(s))
  );
}

app.post('/api/price', async (req, res) => {
  const { shieldRequestId, claimedRegion, userId } = req.body;

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

  // No result yet is not the same as "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 anonymized = isAnonymized(shield.details);
  const countryMatches = shield.country === claimedRegion;

  // Masked session: the claimed region cannot be trusted. Your code falls
  // back to the standard price. ShieldLabs only surfaced the signals.
  if (anonymized) {
    return res.json({
      price: standardPrice(userId),
      region: 'standard',
      reason: 'region_unverified_anonymized',
    });
  }

  // Network country does not match the claimed region. Ask for verification
  // (a billing-address or payment-country check) before honoring the discount.
  if (!countryMatches) {
    return res.json({
      price: standardPrice(userId),
      region: 'standard',
      reason: 'region_mismatch',
      verify: true,
    });
  }

  // Country lines up and the session is not masked: honor the regional price.
  return res.json({ price: regionalPrice(claimedRegion), region: claimedRegion });
});
waitForScore is the shared webhook-cache read (poll the cache, then fall back to a History API read by request_id); the Cookbook defines it once alongside the handler that fills the cache.
The fallback, hold, and verification steps above run in your application. ShieldLabs returns the Country, the Score, and Details. It does not block buyers, decline a price, or pick a region for you.

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 true location. The four bands and their ranges are defined in Risk Scoring; here is how to read the two inputs together.
Network Country vs claimed regionAnonymity signal in DetailsReasonable action
MatchNoneHonor the regional price
MatchVPN / Proxy / Privacy Relay presentVerify before discount; a corporate VPN can match by coincidence
MismatchNoneStandard price, ask for verification
MismatchAny presentStandard price, or hold for review
Unknown (no webhook yet)UnknownStandard price until verified

Signals that make a region unverifiable

The Country tells you where the network exits. The anonymity signals tell you whether that exit can be trusted as the buyer’s real location. The names below are the human-readable labels that appear in Details for visibility and logging; they can change, so match on presence rather than exact text.
Signal in DetailsWhy it breaks a region claim
TorConnection exits through the Tor network, so the country is the exit node, not the buyer.
VPNTraffic routes through a VPN, so the exit country is chosen, not where the buyer sits.
ProxyIP flagged as a proxy. The geolocated country reflects the proxy, not the person.
Privacy RelayiCloud Private Relay relays the connection, so the visible country can differ from the real one.
Datacenter IPIP is in a hosting range. A real shopper on a personal device rarely exits from a datacenter.
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 Country, 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.

Next steps

Signals

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

The Risk Score

How the 0 to 100 score is built, what Details 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, Country, and Details into allow, verify, and hold logic in your app.