The short answer: look up who owns the IP address's network. Home and mobile providers mean a person; clouds, hosting companies and VPN-heavy networks mean a server; the Tor exit list tells you about Tor. Treat server traffic as a reason for extra verification, not an automatic block.
Every IP belongs to a network
The internet is made of autonomous systems (ASNs), each run by one organization: a broadband company, a mobile carrier, a cloud like AWS, a hosting company like Hetzner. The owner tells you far more than the location:
| Network kind | Example | Typical traffic |
|---|---|---|
| ISP (home, mobile, business) | Comcast, Vodafone | Real people |
| Cloud | AWS, Google Cloud, Azure | Scripts, scrapers, servers, some VPNs |
| Hosting | OVH, Hetzner, DigitalOcean | VPN exits, proxies, bots |
| VPN-heavy hosting | M247, Datacamp | Commercial VPN services |
| CDN / relay | Cloudflare, Akamai | Privacy relays such as WARP, iCloud Private Relay |
| Tor | Exit relays | Anonymous browsing |
Browse the network directory to see how specific ASNs are classified.
What you can and can't detect
- Datacenter and cloud IPs: reliably. The owner is public.
- Commercial VPNs: mostly. They run on hosting networks, which are flagged. A VPN running on a home connection (rare) looks like a home user.
- Tor: reliably. The Tor Project publishes its exit list; Veqen refreshes it hourly.
- Residential proxies: not by network. They route through real home connections. Pair IP checks with email and phone checks to catch these.
Code
curl https://veqen-app.com/v1/ip/check \
-H "Authorization: Bearer $VEQEN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"ip": "185.220.101.1"}'
app.set("trust proxy", 1); // so req.ip is the visitor, not your load balancer
app.post("/signup", async (req, res) => {
const ipCheck = await fetch("https://veqen-app.com/v1/ip/check", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.VEQEN_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ ip: req.ip }),
}).then((r) => r.json());
const { hosting, tor, vpn_likely } = ipCheck.flags ?? {};
if (tor) return res.status(403).send("Please turn off Tor to create an account.");
req.session.needsVerification = Boolean(hosting || vpn_likely);
// ...create the account
});
Getting the right IP
The most common mistake is checking your own load balancer's IP. If your app sits behind a proxy, use the visitor address the proxy adds:
- Express:
app.set("trust proxy", 1)thenreq.ip. - Django: read
HTTP_X_FORWARDED_FORonly if your proxy sets it, and take the first address. - Cloudflare: the
CF-Connecting-IPheader.
If Veqen returns "type": "private" with the non_public_ip reason, you're sending an internal address.
Frequently asked questions
Should I block all VPN users?
Usually not. Many legitimate users have a VPN on by default. Use it as one signal, and ask for email or phone verification.
Why does iCloud Private Relay show as a relay, not a VPN?
Relays are run by Apple through CDN networks and mainly protect ordinary Safari users. They're low risk, so they carry a small impact (20).