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
- VPN users trigger false positives. Many legitimate users use VPNs. Don't auto-block based on location alone.
- Mobile IPs are less accurate. Carrier IPs may resolve to a different city than the user's actual location.
- IP geolocation is one signal, not a verdict. Always combine with other factors: device fingerprint, purchase history, email age, etc.
- Legitimate travel exists. Business travelers and digital nomads will trigger impossible travel alerts. Use it as a signal, not a block.
Add Location Data to Your Fraud Stack
Free API with IP, city, country, coordinates, and timezone.
View Documentation →