Skip to content

IP threat intelligence

Every Arcjet decision includes analysis of the client IP. Use it to detect and deny VPNs, proxies, Tor exit nodes, hosting (data center) IPs, and other high-risk networks.

You can enforce that analysis in two ways:

  • A filter rule denies matching traffic together with your other Arcjet rules.
  • The ip field on the decision lets you customize the response in your handler, including the ip.threat assessment when it is present.

IP analysis fields need data from the Arcjet Cloud API, so they are populated when protect() calls the API. For more information about each field, see IP analysis in the SDK reference for your framework.

Arcjet labels each client IP with one or more network types and, when available, a threat intelligence assessment.

SignalFilter fieldJavaScriptPythonGo
VPNip.src.vpnip.isVpn()ip.is_vpn()ip.IsVPN
Proxyip.src.proxyip.isProxy()ip.is_proxy()ip.IsProxy
Torip.src.torip.isTor()ip.is_tor()ip.IsTor
Hosting or data centerip.src.hostingip.isHosting()ip.is_hosting()ip.IsHosting
Privacy relayip.src.relayip.isRelay()ip_details.is_relayip.IsRelay
Known abuseNoneip.isAbuser()ip.is_abuser()ip.IsAbuser
Service nameip.src.serviceip.serviceip_details.serviceip.Service
ASN typeip.src.asnum.typeip.asnTypeip_details.asn_typeip.ASNType

asnType is one of isp, hosting, business, or education. Real users are more often on an ISP or business network than on a hosting provider.

A hosting label means the IP belongs to a hosting provider. That raises the chance the client is automated, but an API used by other servers can see legitimate hosting IPs.

VPN, proxy, and Tor labels mean the client is hiding its origin. People use VPNs for privacy or work, so a VPN match alone is not proof of abuse. Treat it as a risk signal and decide based on the route.

Privacy relays such as Apple Private Relay are paid services tied to an account. They are less likely to be automated than an open proxy. Don’t deny ip.src.relay unless you intend to block those clients.

Filters do not read ip.threat. To deny on risk level, reputation, activity, or entity, inspect the decision after protect().

When threat intelligence is available, the JavaScript SDK exposes it as decision.ip.threat, the Python SDK as decision.ip_details.threat, and the Go SDK as decision.IP.Threat. Responses without an assessment omit it.

FieldJavaScriptPythonGoTypical values
Risk levelriskLevelrisk_levelRiskLevelnone, low, medium, high, critical
ConfidenceconfidenceconfidenceConfidencelow, medium, high
ReputationreputationreputationReputationmalicious, suspicious, known, safe, benign, unknown
Trusted infrastructureisSafeis_safeIsSafetrue when the IP is trusted infrastructure
Network typesnetworkTypesnetwork_typesNetworkTypeshosting, vpn, proxy, tor
ActivitiesactivitiesactivitiesActivitiesbrute_force, scanning, botnet
EntitiesentitiesentitiesEntitiescrawler, ai_crawler, scanner
Entity nameentityNameentity_nameEntityNamea specific entity, when identified
ServiceserviceserviceServicea known service or provider, when identified

Python uses snake_case for the same names. Go uses PascalCase.

For more information about method signatures, availability checks, and geolocation fields, see IP analysis in the Next.js SDK reference, Node.js SDK reference, Astro SDK reference, Bun SDK reference, Nuxt SDK reference, SvelteKit SDK reference, Remix SDK reference, NestJS SDK reference, Python SDK reference, or Go SDK reference. For more information about filter expressions, see the filter field reference.

The following example denies hosting IPs, VPNs, proxies, and Tor. It does not deny privacy relays.

Start the rule in DRY_RUN if you want to log matches before you enforce them.

Call protect() from a route handler, Server Component, or Server Action. This example uses an App Router route handler.

If the decision is denied, return HTTP 403 (or your own challenge). Combine this filter with bot protection and Shield when you want to deny automated clients and common attacks on the same route.

To deny only VPNs and proxies, use a narrower filter:

filter({
deny: ["ip.src.vpn or ip.src.proxy"],
mode: "LIVE",
})

To deny VPNs and leave proxies to another rule, use ip.src.vpn on its own.

Many people use VPNs for privacy or work. On a login or checkout form, that can be a useful signal. On a public marketing page it can block real customers. Prefer DRY_RUN first, then review matches in the Arcjet dashboard.

Hosting IPs are a common source of bots and scripted abuse:

filter({
deny: ["ip.src.hosting"],
mode: "LIVE",
})

You can also match the autonomous system type:

filter({
deny: ['ip.src.asnum.type eq "hosting"'],
mode: "LIVE",
})

Don’t use this on an API that other servers call. Those clients often originate from hosting providers.

Tor exit nodes hide the client origin and are a frequent source of abuse:

filter({
deny: ["ip.src.tor"],
mode: "LIVE",
})

Tor also has legitimate privacy uses. Deny it on sensitive actions such as account creation or payments if that matches your risk tolerance.

ip.threat is the assessment for that IP when Arcjet has one. Check that it exists before you read it. Then deny on the fields that match your risk tolerance.

These patterns run in your handler after protect(). They are not filter expressions.

const decision = await aj.protect(req);
const threat = decision.ip.threat;
if (
threat &&
!threat.isSafe &&
(threat.riskLevel === "high" || threat.riskLevel === "critical")
) {
return new Response("Forbidden", { status: 403 });
}

isSafe / is_safe / IsSafe marks trusted infrastructure. Skip a denial when it is true, even if riskLevel is elevated.

activities lists observed behaviors such as brute_force, scanning, and botnet:

const threat = decision.ip.threat;
const abusive = ["brute_force", "scanning", "botnet"];
if (
threat &&
!threat.isSafe &&
threat.activities.some((activity) => abusive.includes(activity))
) {
return new Response("Forbidden", { status: 403 });
}

reputation is the upstream label. malicious is the strongest deny signal. suspicious is weaker – treat it as a signal to challenge or rate limit rather than an automatic deny on every route.

const threat = decision.ip.threat;
if (threat && !threat.isSafe && threat.reputation === "malicious") {
return new Response("Forbidden", { status: 403 });
}

Deny automated entities on sensitive routes

Section titled “Deny automated entities on sensitive routes”

entities names automated clients such as crawler, ai_crawler, and scanner. entityName is set when Arcjet identifies a specific client.

const threat = decision.ip.threat;
if (
threat &&
!threat.isSafe &&
(threat.entities.includes("scanner") ||
threat.entities.includes("ai_crawler"))
) {
return new Response("Forbidden", { status: 403 });
}

confidence tells you how sure the assessment is (low, medium, or high). Require medium or high before you deny on a public page.

Use decision.ip when you want to customize the response instead of denying in a filter. Check that each field is present before you read it.

const decision = await aj.protect(req);
if (decision.isDenied()) {
return new Response("Forbidden", { status: 403 });
}
const ip = decision.ip;
if (ip.isHosting()) {
// Hosting and data center IPs are often automated clients.
}
if (ip.isVpn() || ip.isProxy() || ip.isTor()) {
// Apply your policy for anonymized traffic.
}
if (ip.isAbuser()) {
// The IP is associated with known abuse.
}
if (ip.isRelay() && ip.hasService() && ip.service === "Apple Private Relay") {
// Apple Private Relay requires a paid iCloud subscription.
}
const threat = ip.threat;
if (threat) {
// threat.riskLevel: none, low, medium, high, critical
// threat.confidence: low, medium, high
// threat.reputation: malicious, suspicious, known, safe, benign, unknown
// threat.isSafe: trusted infrastructure
// threat.networkTypes: hosting, vpn, proxy, tor
// threat.activities: brute_force, scanning, botnet
// threat.entities: crawler, ai_crawler, scanner
// threat.entityName and threat.service when Arcjet identifies them
}

You can also manage filter rules as remote rules from the dashboard or the Arcjet MCP server without a redeploy. Use investigate-ip to look up geo, ASN, and threat intelligence for a suspicious IP, then create a filter to deny it.

Apple Private Relay is the most common privacy relay. If you deny ip.src.relay, you also deny those clients. To allow the relay while still denying other anonymized networks, inspect the service name after protect():

const decision = await aj.protect(req);
if (decision.ip.hasService() && decision.ip.service === "Apple Private Relay") {
// Allow this client.
}
if (decision.ip.isVpn() || decision.ip.isProxy() || decision.ip.isTor()) {
return new Response("Forbidden", { status: 403 });
}

A filter can express the same exception:

filter({
deny: [
'ip.src.vpn or ip.src.proxy or ip.src.tor or (ip.src.relay and ip.src.service ne "Apple Private Relay")',
],
mode: "LIVE",
})

Discussion