Skip to content

Repository files navigation

ipfastcheck-js

TypeScript/JavaScript client for the ipfastcheck.com IP reputation API — detect proxies, VPNs, Tor exit nodes and datacenter ranges, plus geolocation and ASN data.

  • No API key. Nothing to sign up for, no secret binding to configure, no token to rotate.
  • Zero runtime dependencies. Built on the platform fetch.
  • Runs at the edge. Node 18+, Cloudflare Workers, Deno, Bun, browsers.
  • Caching built in, including a Workers KV backend.
  • Fails open by design — a third-party outage must not take your signup flow down.
  • Fully typed, IPv4 and IPv6.
npm install ipfastcheck

Quick start

import { Client } from 'ipfastcheck';

const client = new Client();

const result = await client.check('185.220.101.1');
result.country;      // 'Germany'
result.asn;          // 'AS60729'
result.risk;         // 100
result.isTor;        // true
result.isDatacenter; // true

Making a decision

Most callers want an action, not raw flags. decide() applies a policy and returns allow / review / deny:

const decision = await client.decide(clientIp);

switch (decision.action) {
  case 'deny':   return reject();
  case 'review': return requireEmailVerification();  // step up, don't slam the door
  case 'allow':  return proceed();
}

decision.reasons; // ['risk_100_ge_90', 'proxy', 'datacenter_or_hosting']

review exists because blocking is usually the wrong response. Corporate VPNs, shared CGNAT and mobile carrier NAT all produce "suspicious" addresses for entirely legitimate users. Step-up verification costs an abuser real effort and a real user one click.

import { Client, Policy } from 'ipfastcheck';

const client = new Client({
  policy: new Policy({
    denyRisk: 90,             // risk >= 90 -> deny
    reviewRisk: 50,           // risk >= 50 -> review
    denyTor: true,
    reviewDatacenter: true,
    reviewWhenDegraded: true, // see below
    onError: 'allow',         // fail open
  }),
});

Cloudflare Workers

The whole reason this client uses fetch and an async cache interface. Note that a keyless API means there is no secret to bind — the Worker below is the complete deployment.

import { Client, KVCache, Policy } from 'ipfastcheck';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const client = new Client({
      cache: new KVCache(env.IPFC),        // shared across every isolate in the colo
      policy: new Policy({ denyTor: true }),
      timeoutMs: 2000,                      // keep edge latency bounded
    });

    const ip = request.headers.get('cf-connecting-ip');
    if (!ip) return fetch(request);

    const decision = await client.decide(ip);
    if (decision.action === 'deny') {
      return new Response('Forbidden', { status: 403 });
    }
    return fetch(request);
  },
};
# wrangler.toml
kv_namespaces = [{ binding = "IPFC", id = "<your-namespace-id>" }]

Caching is not optional here: a cold lookup costs roughly 450–600 ms, which is far too much to sit inline on every request. KVCache raises any TTL below 60 s to Workers KV's own minimum.

Trusting the answer

Two things are worth knowing before you gate anything on this API.

Don't branch on isVpn alone. It under-reports. A live example — an address on AS9009 (M247), well-known VPN infrastructure:

{ "flags": { "vpn": false, "proxy": true, "hosting": true }, "risk": 82 }

A naive if (result.isVpn) block lets that straight through. Use the composite signal — risk, netType and isHosting together — which is what Policy does.

Watch for degraded responses. When the upstream reputation backend doesn't answer, the API falls back to a geolocation-only estimate. The response still reports ok: true with a plausible risk score; the only tell is sources.risk === 'geo-only', where vpn is always false and fraudScore is always null.

This client exposes that as result.degraded, caches such answers for two minutes rather than hours, and by default escalates an otherwise-clean degraded result to review instead of allow:

const result = await client.check(ip);
if (result.degraded) {
  // "we didn't get an answer" — not "it's clean"
}

Caching

import { Client, MemoryCache, KVCache, NullCache } from 'ipfastcheck';

new Client();                                     // bounded in-process LRU, 10k entries
new Client({ cache: new MemoryCache(50_000) });
new Client({ cache: new KVCache(env.IPFC) });     // Cloudflare Workers KV
new Client({ cache: new NullCache() });           // disabled

TTLs are picked per result, because volatility differs by orders of magnitude between classes:

Result TTL
Degraded (geo-only) 2 minutes
Tor exit node 15 minutes
Proxy / VPN / datacenter / risk ≥ 50 1 hour
Clean residential 24 hours

Implement the Cache interface for Redis, Deno KV or anything else:

import type { Cache } from 'ipfastcheck';

Checking your own server

checkSelf() needs no argument and no key, which makes it a complete health check for "is my egress address flagged?" — worth knowing before your transactional email starts silently vanishing on a freshly provisioned host.

const self = await client.checkSelf();
if (self.risk > 75) {
  alert(`Our egress IP ${self.ip} is flagged: risk=${self.risk}`);
}

Batch lookups

There is no bulk endpoint, so checkMany() is a polite serial loop. It de-duplicates first, which matters on access logs — the same address recurs constantly, and cache hits are free while fresh lookups are not.

const results = await client.checkMany(addresses, { delayMs: 200 });

for (const [ip, result] of results) {
  if (result.ok && result.risk >= 75) console.log(ip, result.risk, result.asn);
}

API

new Client(options?)

Option Default Purpose
baseUrl https://ipfastcheck.com Endpoint override
timeoutMs 5000 Per-request timeout
retries 2 Extra attempts after the first failure
cache MemoryCache Any Cache implementation
policy Policy Default thresholds for decide()
fetchImpl globalThis.fetch Injectable, for tests

Retries use exponential backoff with jitter. A 4xx other than 429 is never retried.

Method Returns
check(ip, { useCache }) Promise<Result>
checkSelf() Promise<Result> — this host's egress address, never cached
myIp() Promise<string | null>
country(ip) Promise<string | null>
decide(ip, policy?) Promise<Decision>
checkMany(ips, { delayMs }) Promise<Map<string, Result>>

Network failures never throw — check result.ok. Only malformed input throws (InvalidAddressError), since that is a caller bug rather than a runtime condition.

Result

ip · country · countryCode · city · asn · isp · netType · risk · fraudScore · flags · isTor · isProxy · isVpn · isHosting · isDatacenter · isResidential · isMobile · degraded · ageSeconds · fromCache · ok · error · raw

Why keyless matters

Comparable free tiers, as of August 2026:

Service API key HTTPS on free tier VPN/proxy flags free Commercial use
ipfastcheck not required yes yes yes
ip-api.com not required no partial non-commercial only
AbuseIPDB required yes no (abuse score only) yes
ipinfo.io required yes paid add-on yes
MaxMind GeoLite2 account + license key local database no yes

An HTTP-only endpoint cannot be called from a Cloudflare Worker at all, since fetch there requires TLS. That single fact rules out the leading keyless alternative for edge use.

Development

npm install
npm test        # builds, then runs 35 tests on node:test — no network access

TypeScript is the only development dependency; the published package has none.

Related

License

MIT

About

Zero-dependency TypeScript client for the ipfastcheck.com IP reputation API: detect proxies, VPNs, Tor exit nodes and datacenter IPs. No API key. Runs on Node, Cloudflare Workers, Deno and in the browser.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages