The location from lesson 1 is only as honest as the connection it came from. Anyone can rent a server in Frankfurt for the price of a coffee, install a VPN app in thirty seconds, or open Tor Browser and exit from a volunteer's machine in another country. Fraudsters do all three routinely. So do privacy-minded customers, remote employees, journalists and everyone on a corporate network that routes through a security proxy.
That overlap is the whole difficulty. A VPN flag correlates with fraud, but most people behind VPNs are not committing any. If you block on the flag, you lose real customers to stop attackers who will switch to a residential proxy by the afternoon. If you ignore it, you throw away one of the best cheap signals you have. The answer in between is step-up: let anonymised traffic through, but ask it for one more piece of proof on the actions that matter.
01Three different things that all hide an address
They are often lumped together as "proxies", but they mean different things and deserve different responses.
| Signal | What it means | Typical legitimate users | Typical abuse |
|---|---|---|---|
| Datacenter | The address belongs to a hosting or cloud provider, not an ISP | Corporate security gateways, developers testing from servers, your own integrations | Scripted signups, credential stuffing from rented servers, scrapers |
| VPN | The address is a known commercial VPN exit | Privacy-conscious users, travellers, people on public Wi-Fi, remote staff | Hiding location for promo abuse, multi-accounting, geo-restriction bypass |
| Tor | The address is an exit node of the Tor network | Journalists, activists, people in censored countries, the privacy-maximal | Hard-to-trace fraud, account takeover attempts, abuse after other routes are blocked |
Datacenter is the broadest and the noisiest. If you sell an API, a webhook service or any developer tool, a large share of your legitimate traffic comes from datacenters by design: your customers' servers call you from AWS and Google Cloud all day. Datacenter traffic on your signup form is suspicious; datacenter traffic on your API endpoint is normal.
02Check an address for VPN and datacenter use
VPN Proxy Detector (vpndetector) takes an ip on a GET and costs 10 credits.
curl "https://api.apiverve.com/v1/vpndetector?ip=103.62.49.210" \
-H "x-api-key: YOUR_API_KEY"is_vpn is false but is_datacenter is true: a hosting provider, not a commercial VPN.
On every plan you get is_vpn, is_datacenter and checked_on, the date the address was last checked. Higher plans add is_tor, the network's asn and asn_name, and two bands: risk_level (High for Tor exits and VPNs, Moderate for datacenter-only addresses, Low otherwise) and threat_level (how strongly the address anonymises its user). The example above returns is_datacenter: true with risk_level: "Moderate". A residential cable address such as 173.172.81.20 returns false for both flags.
Do not over-read is_vpn: false. Detection works from lists of known exits and hosting ranges. A small or new VPN, a VPN running on residential addresses, or a "residential proxy" network that routes through hijacked home routers will come back clean. Treat a positive as meaningful and a negative as "no evidence", never as "verified residential".
03Check an address against the Tor exit list
Tor Node Detector (tordetect) answers a narrower question with more certainty. The Tor Project publishes the addresses of its exit relays, so membership is a fact rather than a classification.
curl "https://api.apiverve.com/v1/tordetect?ip=185.189.183.143" \
-H "x-api-key: YOUR_API_KEY"isTor is true for an active exit. parsed tells you the address itself was valid.
isTor and parsed are on every plan; ipDetails (country, region, timezone) and the asn/asnName pair are premium. The same address can stop being an exit tomorrow, because relays come and go every day. That shapes your cache: an hour or two for Tor results, not the day you used for location.
When do you need this call, given that VPN Proxy Detector also reports is_tor? Only on plans where is_tor is not included. On those plans, call both in parallel. On plans that include it, VPN Proxy Detector alone covers all three signals for 10 credits instead of 20, and you can drop the second call. The code below handles both cases by reading is_tor when it is a boolean and calling Tor Node Detector only when it comes back null.
A B2B API product sees 40% of requests to its /v1 endpoints arriving from is_datacenter: true addresses. What is the right reading?
04Build the network signal
The first piece is a function that takes an address and returns a compact, cached verdict. It reuses the verve() helper and publicIp() from lesson 1. Two choices matter here: it runs the lookups in parallel, and it fails open. If a lookup times out, the result says "unknown" rather than "suspicious", and your policy decides what unknown means.
import { verve } from './verve.js';
import { publicIp } from './locate.js';
const TTL_MS = 60 * 60 * 1000; // Tor exits change daily; an hour keeps the result honest
const cache = new Map();
export async function networkSignal(raw) {
const ip = publicIp(raw);
if (!ip) return { ip: String(raw), kind: 'private', vpn: false, datacenter: false, tor: false };
const hit = cache.get(ip);
if (hit && hit.expires > Date.now()) return hit.value;
let vpn = null;
try {
vpn = await verve('vpndetector', { ip });
} catch {
// timeout, outage or exhausted credits: report unknown instead of suspicious
}
// is_tor is premium on vpndetector; fall back to the dedicated check when it comes back null.
let tor = typeof vpn?.is_tor === 'boolean' ? vpn.is_tor : null;
if (tor === null) {
try {
tor = (await verve('tordetect', { ip })).isTor;
} catch {
tor = null;
}
}
const value = {
ip,
kind: vpn ? 'checked' : 'unknown',
vpn: vpn?.is_vpn ?? null,
datacenter: vpn?.is_datacenter ?? null,
tor,
};
if (vpn) cache.set(ip, { value, expires: Date.now() + TTL_MS });
return value;
}Failures are not cached: an outage should not pin "unknown" on a visitor for an hour. If your plan never includes is_tor, start both lookups at once with Promise.allSettled to save latency; the sequential fallback above trades a little latency for not spending 10 credits you do not need on plans that do include it.
The Python equivalent uses a thread pool for the parallel case:
import os
import time
from concurrent.futures import ThreadPoolExecutor
import requests
from locate import public_ip
TTL = 60 * 60
_cache = {}
_pool = ThreadPoolExecutor(max_workers=8)
def _get(api, ip):
res = requests.get(
f"https://api.apiverve.com/v1/{api}",
params={"ip": ip},
headers={"x-api-key": os.environ["APIVERVE_API_KEY"]},
timeout=2.5,
)
res.raise_for_status()
return res.json()["data"]
def network_signal(raw):
ip = public_ip(raw)
if ip is None:
return {"ip": str(raw), "kind": "private", "vpn": False, "datacenter": False, "tor": False}
hit = _cache.get(ip)
if hit and hit[1] > time.time():
return hit[0]
vpn_f = _pool.submit(_get, "vpndetector", ip)
tor_f = _pool.submit(_get, "tordetect", ip)
try:
vpn = vpn_f.result()
except Exception:
vpn = None
try:
tor = tor_f.result()["isTor"]
except Exception:
tor = vpn.get("is_tor") if vpn else None
value = {
"ip": ip,
"kind": "checked" if vpn else "unknown",
"vpn": vpn["is_vpn"] if vpn else None,
"datacenter": vpn["is_datacenter"] if vpn else None,
"tor": tor,
}
if vpn:
_cache[ip] = (value, time.time() + TTL)
return valueThis version always runs both calls in parallel, which suits plans without is_tor on the detector. On a plan that includes it, drop the second submit.
05Turn signals into actions, per route
The mistake to avoid is one global rule. The same Tor exit deserves different treatment on your marketing pages, your signup form and your payout settings. Write the policy as data, one entry per sensitive action, and keep the code that applies it boring.
Four actions cover almost every case:
- allow: proceed as normal, but log the signal.
- step_up: proceed only after one more proof. A CAPTCHA for signup, an emailed one-time code for login, 3-D Secure for a card payment, a re-entered password for changing the payout account.
- limit: proceed with reduced exposure. Fewer free-trial credits, a payout hold, a lower first-order limit.
- block: refuse. Reserve it for combinations you have measured to be almost always abuse on that route.
import express from 'express';
import { networkSignal } from './network.js';
const POLICY = {
signup: { tor: 'step_up', vpn: 'allow', datacenter: 'step_up', unknown: 'allow' },
login: { tor: 'step_up', vpn: 'allow', datacenter: 'allow', unknown: 'allow' },
checkout: { tor: 'step_up', vpn: 'step_up', datacenter: 'step_up', unknown: 'allow' },
payout: { tor: 'block', vpn: 'step_up', datacenter: 'step_up', unknown: 'step_up' },
};
const RANK = { allow: 0, limit: 1, step_up: 2, block: 3 };
export function decide(route, s) {
const rules = POLICY[route];
const hits = [];
if (s.kind === 'unknown') hits.push(rules.unknown);
if (s.tor) hits.push(rules.tor);
if (s.vpn) hits.push(rules.vpn);
if (s.datacenter) hits.push(rules.datacenter);
return hits.reduce((worst, a) => (RANK[a] > RANK[worst] ? a : worst), 'allow');
}
const app = express();
app.set('trust proxy', Number(process.env.TRUSTED_PROXIES ?? 0));
app.use(express.json());
function networkGate(route) {
return async (req, res, next) => {
const signal = await networkSignal(req.ip);
req.network = { ...signal, action: decide(route, signal) };
console.log(JSON.stringify({ route, ...req.network }));
if (req.network.action === 'block') {
return res.status(403).json({ error: 'This action is not available from your current connection.' });
}
next();
};
}
app.post('/signup', networkGate('signup'), (req, res) => {
if (req.network.action === 'step_up') {
return res.json({ next: 'captcha' }); // the client shows a challenge, then retries with a token
}
res.json({ next: 'create-account' });
});
app.listen(3000, () => console.log('listening on :3000'));Three details in that code are deliberate. The policy is a plain object, so changing how you treat VPNs on checkout is a one-word diff that shows up in review. The decision takes the strictest of the matching actions, so a Tor exit that is also a datacenter gets the Tor rule. And every decision is logged with the signal that caused it, which is what lets you tune the policy with evidence later instead of guesses.
The block message is honest about the reason without being a tutorial for evasion. "This action is not available from your current connection" tells a legitimate user what to change (turn off the VPN for this step) and tells an attacker nothing they did not already know.
A login arrives from an address where the VPN lookup timed out. Under the policy above, what happens?
06False positives you will meet
Plan for these before they reach your support inbox.
- Corporate gateways. Many companies route all staff traffic through cloud security proxies hosted in datacenters. An entire customer's workforce can arrive as
is_datacenter: true. If you sell to businesses, never block datacenter on login. - Privacy relays built into the platform. Some operating systems and browsers offer built-in relays that egress through large CDN networks. Those addresses may classify as datacenter even though the user did nothing unusual.
- Mobile and shared networks. Carrier-grade NAT puts thousands of phones behind one address. Those are not VPNs, but one bad actor on the same carrier address can taint its reputation, which matters in lesson 3.
- Your own systems. Uptime monitors, CI pipelines and staff testing from cloud machines will trip the datacenter rule. Keep an allow list of your own ranges and check it before the lookup, which also saves the credits.
07Tune with outcomes, not instincts
Run the gate in log-only mode for a couple of weeks before any rule returns step_up or block: compute and log the decision, but always call next() and never challenge. Then join the logs to outcomes you already track, such as chargebacks, accounts banned for abuse and trial accounts that never made a real call, and ask the only question that matters: on this route, what fraction of flagged requests turned out bad, compared with unflagged ones? If Tor signups are ten times as likely to be abusive as the rest, a CAPTCHA is easily justified. If VPN checkouts are no worse than average, leave them alone, whatever your intuition says.
Lesson 3 adds the other half of network risk: not whether an address is hiding, but whether it, or the links your users post, already has a bad record. Continue to IP and domain reputation.