Kamero

Using IP Geolocation for Fraud Detection: A Developer's Guide

IP geolocation is one of the most accessible fraud signals available to developers. It won't catch everything on its own, but combined with other data points, it forms a critical layer in any fraud prevention stack.

Why IP Location Matters for Fraud

Fraudsters often operate from different locations than their victims. A stolen credit card from Texas being used from an IP in Eastern Europe is a strong signal. IP geolocation helps you detect these mismatches automatically.

Pattern 1: Billing Address Mismatch

The most basic check — compare the IP location with the billing or shipping address:

async function checkLocationMismatch(order) {
  const geo = await fetch("https://geo.kamero.ai/api/geo")
    .then(r => r.json());

  const riskFactors = [];

  // Country mismatch
  if (order.billingCountry !== geo.country) {
    riskFactors.push({
      type: "country_mismatch",
      severity: "high",
      detail: `Billing: ${order.billingCountry}, IP: ${geo.country}`,
    });
  }

  // Region mismatch (same country, different state)
  if (order.billingCountry === geo.country &&
      order.billingState !== geo.countryRegion) {
    riskFactors.push({
      type: "region_mismatch",
      severity: "medium",
      detail: `Billing: ${order.billingState}, IP: ${geo.countryRegion}`,
    });
  }

  return riskFactors;
}

Pattern 2: Impossible Travel

If a user logs in from New York and then from Tokyo 30 minutes later, something is wrong. Track login locations and flag physically impossible travel:

function haversineDistance(lat1, lon1, lat2, lon2) {
  const R = 6371; // Earth's radius in km
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLon = (lon2 - lon1) * Math.PI / 180;
  const a = Math.sin(dLat/2) ** 2 +
    Math.cos(lat1 * Math.PI / 180) *
    Math.cos(lat2 * Math.PI / 180) *
    Math.sin(dLon/2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}

async function checkImpossibleTravel(userId, currentGeo) {
  const lastLogin = await db.getLastLogin(userId);
  if (!lastLogin) return null;

  const distance = haversineDistance(
    lastLogin.latitude, lastLogin.longitude,
    parseFloat(currentGeo.latitude),
    parseFloat(currentGeo.longitude)
  );

  const timeDiffHours =
    (Date.now() - lastLogin.timestamp) / (1000 * 60 * 60);

  // Max realistic travel speed: ~900 km/h (commercial flight)
  const maxPossibleDistance = timeDiffHours * 900;

  if (distance > maxPossibleDistance) {
    return {
      type: "impossible_travel",
      severity: "critical",
      detail: `${Math.round(distance)}km in ${timeDiffHours.toFixed(1)}h`,
    };
  }

  return null;
}

Pattern 3: High-Risk Region Detection

Some regions have statistically higher fraud rates. You can adjust risk scores based on the IP's continent or country:

function getRegionRiskScore(geo) {
  // These are examples — adjust based on your actual fraud data
  const highRiskCountries = new Set(["XX", "YY", "ZZ"]);
  const mediumRiskCountries = new Set(["AA", "BB"]);

  let score = 0;

  if (highRiskCountries.has(geo.country)) score += 30;
  else if (mediumRiskCountries.has(geo.country)) score += 15;

  // Mismatched timezone can indicate proxy usage
  const browserTz = order.browserTimezone; // from client
  if (browserTz && browserTz !== geo.timezone) {
    score += 20; // Timezone mismatch suggests VPN/proxy
  }

  return score;
}

Pattern 4: Velocity Checks

Multiple orders from the same IP in a short window is suspicious:

async function checkVelocity(ip) {
  const recentOrders = await db.orders.count({
    ip,
    createdAt: { $gt: new Date(Date.now() - 3600000) }, // last hour
  });

  if (recentOrders > 5) {
    return {
      type: "high_velocity",
      severity: "high",
      detail: `${recentOrders} orders from same IP in 1 hour`,
    };
  }

  return null;
}

Building a Risk Score

Combine all signals into a single risk score:

async function calculateFraudRisk(order) {
  const geo = await fetch("https://geo.kamero.ai/api/geo")
    .then(r => r.json());

  let riskScore = 0;
  const flags = [];

  // Location mismatch
  const mismatches = await checkLocationMismatch(order);
  mismatches.forEach(m => {
    riskScore += m.severity === "high" ? 25 : 10;
    flags.push(m);
  });

  // Impossible travel
  const travel = await checkImpossibleTravel(order.userId, geo);
  if (travel) {
    riskScore += 40;
    flags.push(travel);
  }

  // Region risk
  riskScore += getRegionRiskScore(geo);

  // Velocity
  const velocity = await checkVelocity(geo.ip);
  if (velocity) {
    riskScore += 20;
    flags.push(velocity);
  }

  // Decision
  const decision = riskScore >= 60 ? "block"
    : riskScore >= 30 ? "review"
    : "allow";

  return { riskScore, decision, flags, geo };
}

Limitations to Keep in Mind

Add Location Data to Your Fraud Stack

Free API with IP, city, country, coordinates, and timezone.

View Documentation →