1. Get an API key
Create a free account and copy your API key from the dashboard. The Free plan includes 10,000 lookups a month. Keep the key on your server in an environment variable called VPNBLOCKER_KEY, never in code that runs in the browser.
2. Look up the IP
Put the helpers in their own file, vpnblocker.js. This uses the built-in fetch in Node.js 18 and later, with a 3 second timeout. If the lookup fails it returns null, so you can let the user through.
export async function lookup(ip) {
try {
const res = await fetch(`https://api.vpnblocker.net/v2/json/${encodeURIComponent(ip)}`, {
headers: { 'X-API-KEY': process.env.VPNBLOCKER_KEY },
signal: AbortSignal.timeout(3000),
});
return res.ok ? await res.json() : null;
} catch {
return null; // network error or timeout
}
}
3. Cache the answer
Caching saves lookups and keeps responses fast. An hour is a good length, because Tor exits change every hour.
const cache = new Map(); // ip -> { r, expires }
export async function cachedLookup(ip) {
const hit = cache.get(ip);
if (hit && hit.expires > Date.now()) return hit.r;
const r = await lookup(ip);
if (r) cache.set(ip, { r, expires: Date.now() + 60 * 60 * 1000 });
return r;
}
A Map grows forever and isn’t shared between processes. In production, use a cache with a size limit, such as the lru-cache package, or Redis.
4. Decide: block, challenge or allow
This turns a lookup into one of three actions. It’s the same logic as the example in the docs: change the settings at the top to fit your site.
// Tune these to fit your site.
const BLOCK_CATEGORIES = ['vpn', 'proxy', 'tor']; // add 'hosting' to block data centers too
const BLOCK_AT = 90; // block these categories at or above this confidence
const CHALLENGE_AT = 70; // challenge anything flagged at or above this confidence
const CLEAN_TRUST_AT = 80; // clean answers below this are little-known addresses
export function decide(r, sensitive = false) {
const flagged = r['host-ip'] === true || r.categories.length > 0;
const blockable = r.categories.some((c) => BLOCK_CATEGORIES.includes(c));
if (blockable && r.confidence >= BLOCK_AT) return 'block';
if (flagged && r.confidence >= CHALLENGE_AT) return 'challenge';
if (sensitive && !flagged && r.confidence < CLEAN_TRUST_AT) return 'challenge';
return 'allow';
}
5. Add an Express middleware
Get the visitor’s real IP from req.ip. If your app runs behind a load balancer or reverse proxy, tell Express with trust proxy. Node.js can report IPv4 addresses as ::ffff:1.2.3.4, so strip that prefix.
import express from 'express';
import { cachedLookup, decide } from './vpnblocker.js';
const app = express();
app.set('trust proxy', 1); // only if you're behind exactly one proxy or load balancer
function vpnCheck({ sensitive = false } = {}) {
return async (req, res, next) => {
const ip = req.ip.replace(/^::ffff:/, '');
const r = await cachedLookup(ip);
const action = r ? decide(r, sensitive) : 'allow'; // allow if the lookup failed
if (action === 'block') {
return res.status(403).send('Please turn off your VPN, proxy or Tor and try again.');
}
res.locals.vpnAction = action; // show a captcha when this is 'challenge'
next();
};
}
app.post('/signup', vpnCheck(), signupHandler);
app.post('/checkout', vpnCheck({ sensitive: true }), checkoutHandler);
Only trust a forwarded header if every request really reaches your server through that proxy. Otherwise anyone can send a fake one and pretend to be any IP address.
Behind Cloudflare, read the CF-Connecting-IP header instead of req.ip, but only if all traffic reaches your server through Cloudflare.