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](/filters) 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.

IP threat signals
-----------------

[Section titled “IP threat signals”](#ip-threat-signals)

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

Signal

Filter field

JavaScript

Python

Go

VPN

`ip.src.vpn`

`ip.isVpn()`

`ip.is_vpn()`

`ip.IsVPN`

Proxy

`ip.src.proxy`

`ip.isProxy()`

`ip.is_proxy()`

`ip.IsProxy`

Tor

`ip.src.tor`

`ip.isTor()`

`ip.is_tor()`

`ip.IsTor`

Hosting or data center

`ip.src.hosting`

`ip.isHosting()`

`ip.is_hosting()`

`ip.IsHosting`

Privacy relay

`ip.src.relay`

`ip.isRelay()`

`ip_details.is_relay`

`ip.IsRelay`

Known abuse

None

`ip.isAbuser()`

`ip.is_abuser()`

`ip.IsAbuser`

Service name

`ip.src.service`

`ip.service`

`ip_details.service`

`ip.Service`

ASN type

`ip.src.asnum.type`

`ip.asnType`

`ip_details.asn_type`

`ip.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.

Field

JavaScript

Python

Go

Typical values

Risk level

`riskLevel`

`risk_level`

`RiskLevel`

`none`, `low`, `medium`, `high`, `critical`

Confidence

`confidence`

`confidence`

`Confidence`

`low`, `medium`, `high`

Reputation

`reputation`

`reputation`

`Reputation`

`malicious`, `suspicious`, `known`, `safe`, `benign`, `unknown`

Trusted infrastructure

`isSafe`

`is_safe`

`IsSafe`

`true` when the IP is trusted infrastructure

Network types

`networkTypes`

`network_types`

`NetworkTypes`

`hosting`, `vpn`, `proxy`, `tor`

Activities

`activities`

`activities`

`Activities`

`brute_force`, `scanning`, `botnet`

Entities

`entities`

`entities`

`Entities`

`crawler`, `ai_crawler`, `scanner`

Entity name

`entityName`

`entity_name`

`EntityName`

a specific entity, when identified

Service

`service`

`service`

`Service`

a 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](/reference/nextjs#ip-analysis), [Node.js SDK reference](/reference/nodejs#ip-analysis), [Astro SDK reference](/reference/astro#ip-analysis), [Bun SDK reference](/reference/bun#ip-analysis), [Nuxt SDK reference](/reference/nuxt#ip-analysis), [SvelteKit SDK reference](/reference/sveltekit#ip-analysis), [Remix SDK reference](/reference/remix#ip-analysis), [NestJS SDK reference](/reference/nestjs#ip-analysis), [Python SDK reference](/reference/python#ip-analysis), or [Go SDK reference](/reference/go). For more information about filter expressions, see the [filter field reference](/filters/reference#fields).

Block high-risk networks
------------------------

[Section titled “Block high-risk networks”](#block-high-risk-networks)

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.

(() => { class StarlightTabsRestore extends HTMLElement { connectedCallback() { const starlightTabs = this.closest('starlight-tabs'); if (!(starlightTabs instanceof HTMLElement) || typeof localStorage === 'undefined') return; const syncKey = starlightTabs.dataset.syncKey; if (!syncKey) return; const label = localStorage.getItem(\`starlight-synced-tabs\_\_${syncKey}\`); if (!label) return; const tabs = \[...starlightTabs?.querySelectorAll('\[role="tab"\]')\]; const tabIndexToRestore = tabs.findIndex( (tab) => tab instanceof HTMLAnchorElement && tab.textContent?.trim() === label ); const panels = starlightTabs?.querySelectorAll(':scope > \[role="tabpanel"\]'); const newTab = tabs\[tabIndexToRestore\]; const newPanel = panels\[tabIndexToRestore\]; if (tabIndexToRestore < 1 || !newTab || !newPanel) return; tabs\[0\]?.setAttribute('aria-selected', 'false'); tabs\[0\]?.setAttribute('tabindex', '-1'); panels?.\[0\]?.setAttribute('hidden', 'true'); newTab.removeAttribute('tabindex'); newTab.setAttribute('aria-selected', 'true'); newPanel.removeAttribute('hidden'); } } customElements.define('starlight-tabs-restore', StarlightTabsRestore); })()

*   [JavaScript / TypeScript](#tab-panel-2-0)
*   [Python](#tab-panel-2-1)
*   [Go](#tab-panel-2-2)

*   [Next.js](#tab-panel-0-0)
*   [Node.js](#tab-panel-0-1)
*   [Astro](#tab-panel-0-2)
*   [Bun](#tab-panel-0-3)
*   [Deno](#tab-panel-0-4)
*   [Fastify](#tab-panel-0-5)
*   [NestJS](#tab-panel-0-6)
*   [Nuxt](#tab-panel-0-7)
*   [React Router](#tab-panel-0-8)
*   [Remix](#tab-panel-0-9)
*   [SvelteKit](#tab-panel-0-10)

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

Define the filter in `astro.config.mjs`:

astro.config.mjs

```js
import node from "@astrojs/node";
import arcjet, { filter } from "@arcjet/astro";
import { defineConfig } from "astro/config";

export default defineConfig({
  adapter: node({ mode: "standalone" }),
  env: { validateSecrets: true },
  integrations: [
    arcjet({
      rules: [
        filter({
          // Deny hosting (data center) IPs, VPNs, proxies, and Tor.
          // This does not deny privacy relays such as Apple Private Relay.
          deny: ["ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor"],
          // Block requests with `LIVE`, use `DRY_RUN` to log only.
          mode: "LIVE",
        }),
      ],
    }),
  ],
});
```

Arcjet protection runs on HTTP requests. That is useful for dynamic routes in Astro. It is not useful for static routes, which are pre-rendered at build time.

Astro uses [middleware](https://docs.astro.build/en/guides/middleware/) to intercept requests. That’s where you add Arcjet.

astro-island,astro-slot,astro-static-slot{display:contents}(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).load=e;window.dispatchEvent(new Event("astro:load"));})();(()=>{var g=Object.defineProperty;var w=(a,s,c)=>s in a?g(a,s,{enumerable:!0,configurable:!0,writable:!0,value:c}):a\[s\]=c;var l=(a,s,c)=>w(a,typeof s!="symbol"?s+"":s,c);var E=new Set(\["\_\_proto\_\_","constructor","prototype"\]);{let a={0:t=>y(t),1:t=>c(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(c(t)),5:t=>new Set(c(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>Number.POSITIVE\_INFINITY\*t},s=t=>{let\[p,e\]=t;return p in a?a\[p\](e):void 0},c=t=>t.map(s),y=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map((\[p,e\])=>\[p,s(e)\]));class f extends HTMLElement{constructor(){super(...arguments);l(this,"Component");l(this,"hydrator");l(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island\[ssr\]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let r=this.querySelectorAll("astro-slot"),n={},d=this.querySelectorAll("template\[data-astro-template\]");for(let o of d){let i=o.closest(this.tagName);i!=null&&i.isSameNode(this)&&(n\[o.getAttribute("data-astro-template")||"default"\]=o.innerHTML,o.remove())}for(let o of r){let i=o.closest(this.tagName);i!=null&&i.isSameNode(this)&&(n\[o.getAttribute("name")||"default"\]=o.innerHTML)}let u;try{u=this.hasAttribute("props")?y(JSON.parse(this.getAttribute("props"))):{}}catch(o){let i=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(i+=\` (export ${v})\`),console.error(\`\[hydrate\] Error parsing props for component ${i}\`,this.getAttribute("props"),o),o}let h;await this.hydrator(this)(this.Component,u,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});l(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),r.disconnect(),this.childrenConnectedCallback()},r=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT\_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});r.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}getRetryImportUrl(e){let r=new URL(e,document.baseURI);return r.searchParams.set("astro-retry",Date.now().toString()),r.toString()}async importWithRetry(e){try{return await import(e)}catch(r){return await new Promise(n=>setTimeout(n,1e3)),import(this.getRetryImportUrl(e))}}handleHydrationError(e){let r=this.getAttribute("component-url"),n=new CustomEvent("astro:hydration-error",{cancelable:!0,bubbles:!0,composed:!0,detail:{error:e,componentUrl:r}});this.dispatchEvent(n)&&console.error(\`\[astro-island\] Error hydrating ${r}\`,e)}async start(){let e=JSON.parse(this.getAttribute("opts")),r=this.getAttribute("client");if(Astro\[r\]===void 0){window.addEventListener(\`astro:${r}\`,()=>this.start(),{once:!0});return}try{await Astro\[r\](async()=>{let n=this.getAttribute("renderer-url");try{let\[d,{default:u}\]=await Promise.all(\[this.importWithRetry(this.getAttribute("component-url")),n?this.importWithRetry(n):Promise.resolve({default:()=>()=>{}})\]),h=this.getAttribute("component-export")||"default";if(h.includes(".")){this.Component=d;for(let m of h.split(".")){if(E.has(m)||!this.Component||typeof this.Component!="object"&&typeof this.Component!="function"||!Object.hasOwn(this.Component,m))throw new Error(\`Invalid component export path: ${h}\`);this.Component=this.Component\[m\]}}else{if(E.has(h))throw new Error(\`Invalid component export path: ${h}\`);this.Component=d\[h\]}return this.hydrator=u,this.hydrate}catch(d){return this.handleHydrationError(d),()=>{}}},e,this)}catch(n){this.handleHydrationError(n)}}attributeChangedCallback(){this.hydrate()}}l(f,"observedAttributes",\["props"\]),customElements.get("astro-island")||customElements.define("astro-island",f)}})();

index.ts

```ts
import arcjet, { filter } from "@arcjet/deno";

// Get your Arcjet key at <https://console.arcjet.com>.
// Set it as an environment variable instead of hard coding it.
const arcjetKey = Deno.env.get("ARCJET_KEY");

if (!arcjetKey) {
  throw new Error("Cannot find `ARCJET_KEY` environment variable");
}

const aj = arcjet({
  key: arcjetKey,
  rules: [
    filter({
      // Deny hosting (data center) IPs, VPNs, proxies, and Tor.
      // This does not deny privacy relays such as Apple Private Relay.
      deny: ["ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor"],
      // Block requests with `LIVE`, use `DRY_RUN` to log only.
      mode: "LIVE",
    }),
  ],
});

Deno.serve(
  { port: 3000 },
  aj.handler(async function (request) {
    const decision = await aj.protect(request);

    if (decision.isDenied()) {
      return new Response("Forbidden", { status: 403 });
    }

    return new Response("Hello world");
  }),
);
```

Add the Nuxt module in `nuxt.config.ts`:

nuxt.config.ts

```ts
export default defineNuxtConfig({
  arcjet: {
    key: process.env.ARCJET_KEY,
  },
  compatibilityDate: "2025-07-15",
  modules: ["@arcjet/nuxt"],
});
```

Call `protect()` from a server route.

app/routes/home.tsx

```tsx
import arcjet, { filter } from "@arcjet/react-router";
import type { ReactNode } from "react";
// @ts-expect-error: `react-router` generates such type files.
import type { Route } from "../routes/+types/home";

// Get your Arcjet key at <https://console.arcjet.com>.
// Set it as an environment variable instead of hard coding it.
const arcjetKey = process.env.ARCJET_KEY;

if (!arcjetKey) {
  throw new Error("Cannot find `ARCJET_KEY` environment variable");
}

const aj = arcjet({
  key: arcjetKey,
  rules: [
    filter({
      // Deny hosting (data center) IPs, VPNs, proxies, and Tor.
      // This does not deny privacy relays such as Apple Private Relay.
      deny: ["ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor"],
      // Block requests with `LIVE`, use `DRY_RUN` to log only.
      mode: "LIVE",
    }),
  ],
});

export default function Home(): ReactNode {
  return <>Hello world</>;
}

export async function loader(
  loaderArguments: Route.LoaderArgs,
): Promise<undefined> {
  const decision = await aj.protect(loaderArguments);

  if (decision.isDenied()) {
    throw new Response("Forbidden", { status: 403 });
  }
}
```

app/routes/\_index.tsx

```tsx
import arcjet, { filter } from "@arcjet/remix";
import type { LoaderFunctionArgs } from "@remix-run/node";
import type { ReactNode } from "react";

// Get your Arcjet key at <https://console.arcjet.com>.
// Set it as an environment variable instead of hard coding it.
const arcjetKey = process.env.ARCJET_KEY;

if (!arcjetKey) {
  throw new Error("Cannot find `ARCJET_KEY` environment variable");
}

const aj = arcjet({
  key: arcjetKey,
  rules: [
    filter({
      // Deny hosting (data center) IPs, VPNs, proxies, and Tor.
      // This does not deny privacy relays such as Apple Private Relay.
      deny: ["ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor"],
      // Block requests with `LIVE`, use `DRY_RUN` to log only.
      mode: "LIVE",
    }),
  ],
});

export default function Home(): ReactNode {
  return <>Hello world</>;
}

export async function loader(
  loaderArguments: LoaderFunctionArgs,
): Promise<undefined> {
  const decision = await aj.protect(loaderArguments);

  if (decision.isDenied()) {
    throw new Response("Forbidden", { status: 403 });
  }
}
```

src/hooks.server.ts

```ts
import { env } from "$env/dynamic/private";
import arcjet, { filter } from "@arcjet/sveltekit";
import { type RequestEvent, error } from "@sveltejs/kit";

interface HandleProperties {
  event: RequestEvent;
  resolve: Resolve;
}

type Resolve = (event: RequestEvent) => Promise<Response> | Response;

// Get your Arcjet key at <https://console.arcjet.com>.
// Set it as an environment variable instead of hard coding it.
const arcjetKey = env.ARCJET_KEY;

if (!arcjetKey) {
  throw new Error("Cannot find `ARCJET_KEY` environment variable");
}

const aj = arcjet({
  key: arcjetKey,
  rules: [
    filter({
      // Deny hosting (data center) IPs, VPNs, proxies, and Tor.
      // This does not deny privacy relays such as Apple Private Relay.
      deny: ["ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor"],
      // Block requests with `LIVE`, use `DRY_RUN` to log only.
      mode: "LIVE",
    }),
  ],
});

export async function handle(properties: HandleProperties): Promise<Response> {
  const decision = await aj.protect(properties.event);

  if (decision.isDenied()) {
    return error(403, "Forbidden");
  }

  return properties.resolve(properties.event);
}
```

*   [FastAPI](#tab-panel-1-0)
*   [Flask](#tab-panel-1-1)

main.py

```py
import os

from arcjet import Mode, arcjet, filter_request
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

aj = arcjet(
    key=os.environ["ARCJET_KEY"],  # Get your site key from https://console.arcjet.com
    rules=[
        filter_request(
            mode=Mode.LIVE,  # Blocks requests. Use Mode.DRY_RUN to log only
            # Deny hosting (data center) IPs, VPNs, proxies, and Tor.
            # This does not deny privacy relays such as Apple Private Relay.
            deny=[
                "ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor",
            ],
        ),
    ],
)


@app.get("/")
async def index(request: Request):
    decision = await aj.protect(request)

    if decision.is_denied():
        return JSONResponse({"error": "Forbidden"}, status_code=403)

    return {"message": "Hello world"}
```

app.py

```py
import os

from arcjet import Mode, arcjet_sync, filter_request
from flask import Flask, jsonify, request

app = Flask(__name__)

aj = arcjet_sync(
    key=os.environ["ARCJET_KEY"],  # Get your site key from https://console.arcjet.com
    rules=[
        filter_request(
            mode=Mode.LIVE,  # Blocks requests. Use Mode.DRY_RUN to log only
            # Deny hosting (data center) IPs, VPNs, proxies, and Tor.
            # This does not deny privacy relays such as Apple Private Relay.
            deny=[
                "ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor",
            ],
        ),
    ],
)


@app.get("/")
def index():
    decision = aj.protect(request)

    if decision.is_denied():
        return jsonify(error="Forbidden"), 403

    return jsonify(message="Hello world")
```

The Go SDK protects `net/http` handlers:

main.go

```go
package main

import (
  "log"
  "net/http"
  "os"

  "github.com/arcjet/arcjet-go"
)

var aj = must(arcjet.NewClient(arcjet.Config{
  Key: os.Getenv("ARCJET_KEY"),
  Rules: []arcjet.Rule{
    arcjet.Filter(arcjet.FilterOptions{
      // Deny hosting (data center) IPs, VPNs, proxies, and Tor.
      // This does not deny privacy relays such as Apple Private Relay.
      Deny: []string{
        "ip.src.hosting or ip.src.vpn or ip.src.proxy or ip.src.tor",
      },
      // Block requests with ModeLive, use ModeDryRun to log only.
      Mode: arcjet.ModeLive,
    }),
  },
}))

func handler(w http.ResponseWriter, r *http.Request) {
  decision, err := aj.Protect(r.Context(), r)
  if err != nil {
    // Fail-open: ERROR decision plus err. Log it and continue.
    log.Printf("arcjet: %v", err)
  } else if decision.IsDenied() {
    http.Error(w, "Forbidden", http.StatusForbidden)
    return
  }

  w.WriteHeader(http.StatusOK)
  _, _ = w.Write([]byte("Hello world"))
}

func main() {
  http.HandleFunc("/", handler)
  log.Fatal(http.ListenAndServe(":3000", nil))
}

func must[T any](value T, err error) T {
  if err != nil {
    panic(err)
  }
  return value
}
```

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

Block VPN and proxy traffic
---------------------------

[Section titled “Block VPN and proxy traffic”](#block-vpn-and-proxy-traffic)

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

*   [JavaScript / TypeScript](#tab-panel-3-0)
*   [Python](#tab-panel-3-1)
*   [Go](#tab-panel-3-2)

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

```py
filter_request(
    deny=["ip.src.vpn or ip.src.proxy"],
    mode=Mode.LIVE,
)
```

```go
arcjet.Filter(arcjet.FilterOptions{
  Deny: []string{"ip.src.vpn or ip.src.proxy"},
  Mode: arcjet.ModeLive,
})
```

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](https://console.arcjet.com).

Block hosting and data center IPs
---------------------------------

[Section titled “Block hosting and data center IPs”](#block-hosting-and-data-center-ips)

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

*   [JavaScript / TypeScript](#tab-panel-4-0)
*   [Python](#tab-panel-4-1)
*   [Go](#tab-panel-4-2)

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

```py
filter_request(
    deny=["ip.src.hosting"],
    mode=Mode.LIVE,
)
```

```go
arcjet.Filter(arcjet.FilterOptions{
  Deny: []string{"ip.src.hosting"},
  Mode: arcjet.ModeLive,
})
```

You can also match the autonomous system type:

*   [JavaScript / TypeScript](#tab-panel-5-0)
*   [Python](#tab-panel-5-1)
*   [Go](#tab-panel-5-2)

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

```py
filter_request(
    deny=['ip.src.asnum.type eq "hosting"'],
    mode=Mode.LIVE,
)
```

```go
arcjet.Filter(arcjet.FilterOptions{
  Deny: []string{`ip.src.asnum.type eq "hosting"`},
  Mode: arcjet.ModeLive,
})
```

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

Block Tor traffic
-----------------

[Section titled “Block Tor traffic”](#block-tor-traffic)

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

*   [JavaScript / TypeScript](#tab-panel-6-0)
*   [Python](#tab-panel-6-1)
*   [Go](#tab-panel-6-2)

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

```py
filter_request(
    deny=["ip.src.tor"],
    mode=Mode.LIVE,
)
```

```go
arcjet.Filter(arcjet.FilterOptions{
  Deny: []string{"ip.src.tor"},
  Mode: arcjet.ModeLive,
})
```

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

Deny with threat intelligence
-----------------------------

[Section titled “Deny with threat intelligence”](#deny-with-threat-intelligence)

`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.

### Deny high or critical risk

[Section titled “Deny high or critical risk”](#deny-high-or-critical-risk)

*   [JavaScript / TypeScript](#tab-panel-7-0)
*   [Python](#tab-panel-7-1)
*   [Go](#tab-panel-7-2)

```ts
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 });
}
```

```py
decision = await aj.protect(request)
details = decision.ip_details
threat = details.threat if details else None

if (
    threat
    and not threat.is_safe
    and threat.risk_level in ("high", "critical")
):
    return JSONResponse({"error": "Forbidden"}, status_code=403)
```

```go
decision, err := aj.Protect(r.Context(), r)
if err != nil {
  log.Printf("arcjet: %v", err)
} else if threat := decision.IP.Threat; threat != nil && !threat.IsSafe &&
  (threat.RiskLevel == "high" || threat.RiskLevel == "critical") {
  http.Error(w, "Forbidden", http.StatusForbidden)
  return
}
```

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

### Deny known abuse activity

[Section titled “Deny known abuse activity”](#deny-known-abuse-activity)

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

*   [JavaScript / TypeScript](#tab-panel-8-0)
*   [Python](#tab-panel-8-1)
*   [Go](#tab-panel-8-2)

```ts
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 });
}
```

```py
threat = decision.ip_details.threat if decision.ip_details else None
abusive = {"brute_force", "scanning", "botnet"}

if threat and not threat.is_safe and abusive.intersection(threat.activities):
    return JSONResponse({"error": "Forbidden"}, status_code=403)
```

```go
if threat := decision.IP.Threat; threat != nil && !threat.IsSafe &&
  (slices.Contains(threat.Activities, "brute_force") ||
    slices.Contains(threat.Activities, "scanning") ||
    slices.Contains(threat.Activities, "botnet")) {
  http.Error(w, "Forbidden", http.StatusForbidden)
  return
}
```

### Deny malicious reputation

[Section titled “Deny malicious reputation”](#deny-malicious-reputation)

`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.

*   [JavaScript / TypeScript](#tab-panel-9-0)
*   [Python](#tab-panel-9-1)
*   [Go](#tab-panel-9-2)

```ts
const threat = decision.ip.threat;

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

```py
threat = decision.ip_details.threat if decision.ip_details else None

if threat and not threat.is_safe and threat.reputation == "malicious":
    return JSONResponse({"error": "Forbidden"}, status_code=403)
```

```go
if threat := decision.IP.Threat; threat != nil && !threat.IsSafe &&
  threat.Reputation == "malicious" {
  http.Error(w, "Forbidden", http.StatusForbidden)
  return
}
```

### Deny automated entities on sensitive routes

[Section titled “Deny automated entities on sensitive routes”](#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.

*   [JavaScript / TypeScript](#tab-panel-10-0)
*   [Python](#tab-panel-10-1)
*   [Go](#tab-panel-10-2)

```ts
const threat = decision.ip.threat;

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

```py
threat = decision.ip_details.threat if decision.ip_details else None

if threat and not threat.is_safe and (
    "scanner" in threat.entities or "ai_crawler" in threat.entities
):
    return JSONResponse({"error": "Forbidden"}, status_code=403)
```

```go
if threat := decision.IP.Threat; threat != nil && !threat.IsSafe &&
  (slices.Contains(threat.Entities, "scanner") ||
    slices.Contains(threat.Entities, "ai_crawler")) {
  http.Error(w, "Forbidden", http.StatusForbidden)
  return
}
```

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

Inspect a decision
------------------

[Section titled “Inspect a decision”](#inspect-a-decision)

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.

*   [JavaScript / TypeScript](#tab-panel-11-0)
*   [Python](#tab-panel-11-1)
*   [Go](#tab-panel-11-2)

```ts
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
}
```

```py
decision = await aj.protect(request)

if decision.is_denied():
    return JSONResponse({"error": "Forbidden"}, status_code=403)

ip = decision.ip

if ip.is_hosting():
    # Hosting and data center IPs are often automated clients.
    pass

if ip.is_vpn() or ip.is_proxy() or ip.is_tor():
    # Apply your policy for anonymized traffic.
    pass

if ip.is_abuser():
    # The IP is associated with known abuse.
    pass

if (
    decision.ip_details
    and decision.ip_details.is_relay
    and decision.ip_details.service == "Apple Private Relay"
):
    # Apple Private Relay requires a paid iCloud subscription.
    pass

details = decision.ip_details
threat = details.threat if details else None

if threat:
    # threat.risk_level, threat.confidence, threat.reputation
    # threat.is_safe, threat.network_types, threat.activities
    # threat.entities, threat.entity_name, threat.service
    pass
```

```go
decision, err := aj.Protect(r.Context(), r)
if err != nil {
  http.Error(w, "Unavailable", http.StatusServiceUnavailable)
  return
}

if decision.IsDenied() {
  http.Error(w, "Forbidden", http.StatusForbidden)
  return
}

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.Service == "Apple Private Relay" {
  // Apple Private Relay requires a paid iCloud subscription.
}

if threat := ip.Threat; threat != nil {
  // threat.RiskLevel, threat.Confidence, threat.Reputation
  // threat.IsSafe, threat.NetworkTypes, threat.Activities
  // threat.Entities, threat.EntityName, threat.Service
}
```

You can also manage filter rules as [remote rules](/remote-rules) from the dashboard or the [Arcjet MCP server](/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.

Allow a trusted relay
---------------------

[Section titled “Allow a trusted relay”](#allow-a-trusted-relay)

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()`:

*   [JavaScript / TypeScript](#tab-panel-12-0)
*   [Python](#tab-panel-12-1)
*   [Go](#tab-panel-12-2)

```ts
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 });
}
```

```py
decision = await aj.protect(request)

if (
    decision.ip_details
    and decision.ip_details.service == "Apple Private Relay"
):
    # Allow this client.
    pass
elif decision.ip.is_vpn() or decision.ip.is_proxy() or decision.ip.is_tor():
    return JSONResponse({"error": "Forbidden"}, status_code=403)
```

```go
decision, err := aj.Protect(r.Context(), r)
if err != nil {
  log.Printf("arcjet: %v", err)
} else if decision.IP.Service == "Apple Private Relay" {
  // Allow this client.
} else if decision.IP.IsVPN || decision.IP.IsProxy || decision.IP.IsTor {
  http.Error(w, "Forbidden", http.StatusForbidden)
  return
}
```

A filter can express the same exception:

*   [JavaScript / TypeScript](#tab-panel-13-0)
*   [Python](#tab-panel-13-1)
*   [Go](#tab-panel-13-2)

```ts
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",
})
```

```py
filter_request(
    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=Mode.LIVE,
)
```

```go
arcjet.Filter(arcjet.FilterOptions{
  Deny: []string{
    `ip.src.vpn or ip.src.proxy or ip.src.tor or (ip.src.relay and ip.src.service ne "Apple Private Relay")`,
  },
  Mode: arcjet.ModeLive,
})
```

What next?
----------

[Section titled “What next?”](#what-next)

[Filters](/filters) Write expressions that deny VPNs, proxies, countries, and other request fields.

[Bot protection](/bot-protection) Detect and deny automated clients, including bots on hosting IPs.

[IP geolocation](/blueprints/ip-geolocation) Customize responses from country, region, and city fields.

[VPN and proxy detection](/blueprints/vpn-proxy-detection) A shorter blueprint that focuses on VPN and proxy checks.

[SDK reference](/reference/nextjs) IP analysis fields and method signatures for each SDK.

Discussion
----------