Most IP-based fraud checks stop at geolocation and VPN flags. That misses a layer of context sitting right there in the data: the autonomous system number.
A single ASN lookup tells you which organization controls the IP block your visitor is connecting from. It tells you whether that IP belongs to a residential ISP like Comcast (AS7922), a cloud provider like Hetzner (AS24940), or a VPN operator. That distinction matters. A login attempt from a Comcast residential IP and one from a Hetzner datacenter IP carry very different risk profiles, even if both geolocate to the same city.
This tutorial builds a risk-scoring function that combines ASN type classification with IP security signals (VPN detection, proxy detection, threat scoring) in a single API call. You will walk away with Express middleware you can drop into a Node.js app, plus equivalent Python code.
TL;DR
- Every IP address belongs to exactly one ASN, which identifies the organization controlling that IP block
- The ASN's
typefield (ISP, HOSTING, BUSINESS, EDUCATION) is a strong fraud signal because most automated attacks originate from HOSTING-type networks - Combining ASN type with VPN/proxy detection and threat scoring in one API call gives you a composite risk score without multiple vendor roundtrips
- The tutorial builds a
calculateRiskScore()function and Express middleware with caching, fail-open logic, and proper error handling
ASN data adds network-level context that geolocation and VPN flags alone cannot provide. Knowing that an IP belongs to a datacenter hosting provider changes a fraud decision differently than knowing it geolocates to Dallas. This tutorial shows how to make that distinction programmatically and act on it.
What an ASN tells you that geolocation doesn't
An autonomous system (AS) is a network, or collection of networks, operated by a single organization under a single routing policy. Each AS gets a unique number from a regional internet registry (ARIN, RIPE, APNIC, LACNIC, AFRINIC). There are roughly 115,000 active ASNs as of 2026.
The practical value for fraud detection is the ASN metadata, not the number itself. When you look up an IP's ASN, you get the organization name, its type classification, the country of registration, and the RIR that issued it. The type field is where the fraud signal lives.
Here is what a lookup returns for a Hetzner datacenter IP (49.12.0.0):
{
"asn": {
"as_number": "AS24940",
"organization": "Hetzner Online GmbH",
"type": "HOSTING",
"country": "DE",
"domain": "hetzner.com",
"rir": "RIPE"
}
}
And here is the same lookup for a Google corporate IP (8.8.8.8):
{
"asn": {
"as_number": "AS15169",
"organization": "Google LLC",
"type": "BUSINESS",
"country": "US",
"domain": "google.com",
"rir": "ARIN"
}
}
The type field classifies the ASN into categories like ISP (consumer broadband), HOSTING (cloud/datacenter), BUSINESS (corporate networks), and EDUCATION (university networks). This classification comes from analyzing the ASN's routing behavior, registered purpose, and allocation history.
Why does this matter for fraud? The Cybercrime Information Center's Q1 2025 data tracked malware activity by ASN and found DigitalOcean (AS14061) ranked #5 globally with 29,089 unique malware-hosting IPs from only 3 million routed addresses. Amazon's AS16509 had 13,459 malware addresses but spread across 210 million routed IPs. Hosting-type ASNs are disproportionately represented in malicious activity relative to their size.
A related ARIN-funded study by the DNS Research Federation (published January 2026) found that roughly 70% of reported malware URLs rely solely on IP addresses rather than domain names. That means DNS-based detection misses them entirely, and IP-level enrichment (including ASN data) is one of the few signals that catches them.
ASN type as a fraud signal
Here is how to interpret each type in a fraud context:
ISP is the expected type for real human users. Comcast (AS7922), Verizon (AS701), Deutsche Telekom (AS3320). Residential broadband. The vast majority of legitimate traffic comes from ISP-type ASNs. Low risk by itself, though compromised home machines and residential proxies use ISP ASNs too.
HOSTING is the high-attention category. AWS, Hetzner, DigitalOcean, OVH, Linode. Cloud VMs and dedicated servers. Legitimate users don't typically browse your app from a Hetzner VPS. When a login or signup comes from a HOSTING-type ASN, it is worth adding friction: a CAPTCHA, rate limiting, or manual review depending on your risk tolerance.
BUSINESS covers corporate networks. A login from a BUSINESS-type ASN is usually a company employee on a corporate VPN or office network. Medium context. Could be fine. Could be a compromised machine on a corporate network. Treat as neutral unless other signals (threat score, proxy flag) elevate it.
EDUCATION means university or research networks. Often legitimate. Also a common source of scanning and experimentation from campus WiFi. Low-to-medium risk.
Pitfall: ASN type is a prior, not a verdict. Blocking all HOSTING-type traffic would lock out developers who test from cloud VMs, monitoring services that health-check your endpoints, and legitimate webhook callbacks from SaaS platforms. Use ASN type to adjust a risk score, not as a binary block rule.
Fetch ASN and security data for an IP
Several providers return ASN data in their IP intelligence APIs: ipinfo, MaxMind GeoIP2, Team Cymru, IPLocate, and IPGeolocation all support ASN lookups. I will use ipgeolocation.io for the examples because a single call to the /v3/ipgeo endpoint returns ASN classification and security signals (VPN, proxy, threat score) together, which saves you a second API roundtrip.
Note: ASN data is on the free tier (1,000 requests/day). The security signals (
include=security) require a paid plan starting at $19/month for 150K requests. The combined call is what makes the fraud pipeline work, so the examples use the paid path.
curl
curl -s "https://api.ipgeolocation.io/v3/ipgeo?\
apiKey=${IPGEO_API_KEY}&\
ip=49.12.0.1&\
include=security&\
fields=asn,security.threat_score,security.is_vpn,security.is_proxy,security.is_tor,security.is_residential_proxy"
Response (trimmed to the fields we care about):
{
"ip": "49.12.0.1",
"asn": {
"as_number": "AS24940",
"organization": "Hetzner Online GmbH",
"type": "HOSTING",
"country": "DE",
"domain": "hetzner.com",
"rir": "RIPE"
},
"security": {
"threat_score": 80,
"is_vpn": false,
"is_proxy": false,
"is_tor": false,
"is_residential_proxy": false
}
}
Hetzner datacenter IP. HOSTING type. Threat score of 80. No VPN or proxy flags, but the ASN classification alone tells you this is not a consumer browsing session.
Node.js
// fetchIpIntel.js
// Returns ASN + security data for a given IP address
const API_KEY = process.env.IPGEO_API_KEY;
const BASE_URL = 'https://api.ipgeolocation.io/v3/ipgeo';
async function fetchIpIntel(ip) {
const params = new URLSearchParams({
apiKey: API_KEY,
ip,
include: 'security',
fields: 'asn,security.threat_score,security.is_vpn,security.is_proxy,security.is_tor,security.is_residential_proxy',
});
try {
const res = await fetch(`${BASE_URL}?${params}`, {
signal: AbortSignal.timeout(1500), // Fail fast; don't hold the request
});
if (!res.ok) {
throw new Error(`API returned ${res.status}: ${res.statusText}`);
}
return await res.json();
} catch (err) {
// Network timeout, DNS failure, or API error
console.error(`IP intel lookup failed for ${ip}:`, err.message);
return null;
}
}
export { fetchIpIntel };
Python
# fetch_ip_intel.py
# Returns ASN + security data for a given IP address
import os
import requests
API_KEY = os.environ.get("IPGEO_API_KEY")
BASE_URL = "https://api.ipgeolocation.io/v3/ipgeo"
def fetch_ip_intel(ip: str) -> dict | None:
try:
resp = requests.get(
BASE_URL,
params={
"apiKey": API_KEY,
"ip": ip,
"include": "security",
"fields": "asn,security.threat_score,security.is_vpn,security.is_proxy,security.is_tor,security.is_residential_proxy",
},
timeout=(1.0, 1.5), # (connect, read) timeouts in seconds
)
resp.raise_for_status()
return resp.json()
except requests.RequestException as exc:
print(f"IP intel lookup failed for {ip}: {exc}")
return None
Both implementations include timeouts, error handling, and environment variable API keys. If the API is unreachable, they return null/None instead of raising. This matters for the next step.
Build a risk-scoring function
The risk function takes the combined ASN + security response and returns a numeric score with a decision. The thresholds below are a starting point. Tune them to your traffic.
Node.js
// riskScore.js
// Composite risk scoring from ASN type + security signals
const ASN_TYPE_RISK = {
HOSTING: 30, // Datacenter/cloud: high attention
BUSINESS: 10, // Corporate network: some attention
EDUCATION: 5, // University: low attention
ISP: 0, // Residential broadband: expected
};
function calculateRiskScore(data) {
if (!data?.asn || !data?.security) {
// Missing data = unknown risk. Return a middle score, not zero.
return { score: 50, decision: 'CHALLENGE', reason: 'incomplete data' };
}
let score = 0;
const factors = [];
// ASN type classification
const asnType = data.asn.type ?? 'UNKNOWN';
const asnRisk = ASN_TYPE_RISK[asnType] ?? 15;
score += asnRisk;
if (asnRisk > 0) factors.push(`asn_type:${asnType}`);
// VPN detection
if (data.security.is_vpn) {
score += 20;
factors.push('vpn');
}
// Proxy detection (non-VPN proxies)
if (data.security.is_proxy) {
score += 25;
factors.push('proxy');
}
// Residential proxy: hardest to detect, highest risk when present
if (data.security.is_residential_proxy) {
score += 30;
factors.push('residential_proxy');
}
// Tor exit node
if (data.security.is_tor) {
score += 25;
factors.push('tor');
}
// API threat score (0-100), scaled to contribute up to 30 points
const threatScore = data.security.threat_score ?? 0;
const threatContribution = Math.round((threatScore / 100) * 30);
score += threatContribution;
if (threatContribution > 10) factors.push(`threat:${threatScore}`);
// Cap at 100
score = Math.min(score, 100);
// Decision thresholds
let decision;
if (score < 20) decision = 'ALLOW';
else if (score < 50) decision = 'CHALLENGE';
else decision = 'BLOCK';
return { score, decision, factors };
}
export { calculateRiskScore };
A few design choices worth calling out:
The ASN type contributes 0 to 30 base points. That is enough to push a borderline IP into CHALLENGE territory, but not enough to trigger BLOCK on its own. A Hetzner IP with no VPN flags and a low threat score scores 30 (CHALLENGE). Add a VPN flag and it jumps to 50 (BLOCK). That graduated response is the point.
Residential proxies get the highest individual weight (30 points) because they are the hardest signal to detect and almost always indicate fraud or scraping when present.
Missing data defaults to score 50 and CHALLENGE. This is a judgment call. If you can't determine the risk, adding friction (CAPTCHA, email verification) is safer than either allowing blindly or blocking based on no evidence.
Python
# risk_score.py
# Composite risk scoring from ASN type + security signals
ASN_TYPE_RISK = {
"HOSTING": 30,
"BUSINESS": 10,
"EDUCATION": 5,
"ISP": 0,
}
def calculate_risk_score(data: dict | None) -> dict:
if not data or "asn" not in data or "security" not in data:
return {"score": 50, "decision": "CHALLENGE", "reason": "incomplete data"}
score = 0
factors = []
# ASN type
asn_type = data["asn"].get("type", "UNKNOWN")
asn_risk = ASN_TYPE_RISK.get(asn_type, 15)
score += asn_risk
if asn_risk > 0:
factors.append(f"asn_type:{asn_type}")
# Security flags
sec = data["security"]
if sec.get("is_vpn"):
score += 20
factors.append("vpn")
if sec.get("is_proxy"):
score += 25
factors.append("proxy")
if sec.get("is_residential_proxy"):
score += 30
factors.append("residential_proxy")
if sec.get("is_tor"):
score += 25
factors.append("tor")
# Threat score contribution (up to 30 points)
threat = sec.get("threat_score", 0)
threat_contrib = round((threat / 100) * 30)
score += threat_contrib
if threat_contrib > 10:
factors.append(f"threat:{threat}")
score = min(score, 100)
if score < 20:
decision = "ALLOW"
elif score < 50:
decision = "CHALLENGE"
else:
decision = "BLOCK"
return {"score": score, "decision": decision, "factors": factors}
Wire it into Express middleware
Here is how the pieces fit together in a Node.js backend. The middleware runs on every request (or on sensitive routes like /login and /signup), caches results per IP, and attaches the risk assessment to req for downstream handlers.
// ipRiskMiddleware.js
import { fetchIpIntel } from './fetchIpIntel.js';
import { calculateRiskScore } from './riskScore.js';
import { createClient } from 'redis';
const redis = createClient({ url: process.env.REDIS_URL });
redis.on('error', (err) => console.error('Redis error:', err));
await redis.connect();
const CACHE_TTL = 900; // 15 minutes for security data
async function ipRiskMiddleware(req, res, next) {
// Trust proxy must be configured in Express for this to work behind a load balancer.
// Without it, req.ip returns the load balancer's IP, not the client's.
const clientIp = req.ip;
if (!clientIp) {
req.riskScore = { score: 0, decision: 'ALLOW', factors: ['no_ip'] };
return next();
}
try {
// Check cache first
const cached = await redis.get(`ip-risk:${clientIp}`);
if (cached) {
req.riskScore = JSON.parse(cached);
return next();
}
const intel = await fetchIpIntel(clientIp);
const risk = calculateRiskScore(intel);
// Cache the result
await redis.setEx(`ip-risk:${clientIp}`, CACHE_TTL, JSON.stringify(risk));
req.riskScore = risk;
} catch (err) {
// Fail open: if anything breaks, let the request through.
// For payment endpoints, you might want fail-closed instead.
console.error('Risk middleware error:', err.message);
req.riskScore = { score: 0, decision: 'ALLOW', factors: ['error_failopen'] };
}
next();
}
export { ipRiskMiddleware };
Usage in your Express app:
import express from 'express';
import { ipRiskMiddleware } from './ipRiskMiddleware.js';
const app = express();
// Enable trust proxy if behind nginx, Cloudflare, AWS ALB, etc.
// Without this, req.ip is wrong and everything downstream breaks.
app.set('trust proxy', true);
// Apply to sensitive routes
app.use('/login', ipRiskMiddleware);
app.use('/signup', ipRiskMiddleware);
app.post('/login', (req, res) => {
const { score, decision, factors } = req.riskScore;
if (decision === 'BLOCK') {
// Log the attempt; don't tell the user why
console.warn(`Blocked login: ip=${req.ip} score=${score} factors=${factors}`);
return res.status(403).json({ error: 'Request blocked' });
}
if (decision === 'CHALLENGE') {
// Require additional verification (CAPTCHA, email OTP, etc.)
return res.status(200).json({ requiresChallenge: true, challengeType: 'captcha' });
}
// Proceed with normal login flow
// ... your auth logic here
});
Tip: The middleware defaults to fail-open: if Redis is down, the API is unreachable, or anything else breaks, the request goes through with
decision: 'ALLOW'. This is the right default for most web apps where availability matters more than blocking every threat. For payment processing, checkout flows, or high-value account actions, switch to fail-closed (return CHALLENGE or BLOCK on error) so an API outage does not leave a security gap.
What this catches (and what it doesn't)
Worth being honest about limits. ASN-based risk scoring is strongest against:
Datacenter-originating attacks. Credential stuffing bots running on AWS, Hetzner, or DigitalOcean VMs get flagged immediately because their ASN type is HOSTING. Combined with the threat score, these typically score 50+ and land in BLOCK territory. This is the highest-volume category of automated abuse, and ASN type catches it without maintaining IP blocklists.
VPN and proxy traffic. The security flags add 20 to 25 points on top of whatever the ASN type contributes. A consumer VPN like NordVPN on an ISP-type ASN still gets flagged through the VPN detection layer, and the combined score pushes it into CHALLENGE range.
Bot and scanner traffic. The API's threat_score aggregates signals from threat feeds, behavioral profiling, and known attacker databases. Bots hitting your endpoints from cloud infrastructure combine a high threat score with a HOSTING ASN type, which stacks into the BLOCK range.
The approach is weakest against:
Residential proxies. These are the hardest threat to catch because they route traffic through real ISP connections. The ASN type shows ISP (expected for legitimate users), and the IP itself may belong to an unsuspecting home user whose device is part of a proxy network. The is_residential_proxy flag helps, but detection rates for residential proxies are lower across the industry than for VPNs or datacenter proxies.
Compromised home machines. A botnet node on a residential Comcast connection has a clean ASN type (ISP) and may not trigger threat scoring until it is reported. IP-level signals alone are insufficient here; you need behavioral analytics at the application layer (login velocity, credential patterns, session anomalies).
Legitimate cloud traffic. Webhook callbacks from Stripe, monitoring probes from Datadog, or API calls from a customer's cloud deployment all come from HOSTING-type ASNs. The scoring function correctly flags them. You will need allowlists for known service IPs or a separate path for server-to-server calls that skips the risk middleware.
A few extra notes
Cache aggressively. ASN data changes rarely. An IP's autonomous system stays the same for months or years. Security signals change faster but are still valid for 5 to 15 minutes. The middleware above caches combined results for 15 minutes, which keeps API usage well within plan limits even at high traffic volumes.
IPv6 works the same way. The API accepts both IPv4 and IPv6 addresses. The ASN lookup returns the same fields. No code changes needed, but make sure your IP extraction handles both formats (Express does this by default when trust proxy is configured).
Log the decision, not the raw IP. IP addresses are personal data under GDPR. If you operate in the EU or serve EU users, log the risk score, decision, and contributing factors rather than storing the raw IP alongside user records. The risk assessment is what you need for audit trails; the IP itself is not necessary after the decision is made.
Bulk lookups for historical analysis. If you are processing logs or running batch fraud analysis, the API supports up to 50,000 IPs per POST request on the bulk endpoint. That is useful for retroactive analysis: pull your last 30 days of login IPs, run them through the bulk endpoint, and see what your current scoring function would have caught.
Next steps
Start with logging-only mode. Apply the middleware to your login and signup routes but only log the risk scores for a week before you enforce any blocking. Compare the scores against your existing fraud data to tune the thresholds. The defaults in the code above (ALLOW < 20, CHALLENGE < 50, BLOCK >= 50) are conservative. Your traffic will tell you whether to tighten or loosen them.
If you need the risk data without the API call on every request, the downloadable IP-to-ASN and IP Security databases let you run lookups locally. Latency drops to microseconds, but you take on the update cycle yourself.
Top comments (0)