# Geofencing a US state by IP: every state has VPN exits

Every US state holds VPN and residential proxy exits, so an IP-only geofence lets them through. Build one in Express that geolocates, then checks the address.

In May, a Nevada court ordered Kalshi to stop offering sports, election and entertainment contracts to anyone located in Nevada. Kalshi built a geofence on the visitor's IP address, and the state went back to court a month later saying it didn't work. IP geolocation puts most visitors in the right state; it fails when the address isn't the visitor's own, and every US state and DC holds thousands of VPN exits and residential proxy exits. This post counts them, then builds a Nevada geofence in Express that geolocates the address and refuses to trust the answer when the address belongs to a VPN, a proxy or a server. It's about the engineering, not legal advice.

## What Nevada said about Kalshi's geofence

The court's order of May 18 required Kalshi to geofence its contracts so they weren't offered to anyone in Nevada. On June 12 the Nevada Gaming Control Board asked the court to hold Kalshi in contempt ([the Board's release](https://www.gaming.nv.gov/siteassets/content/about/press-release/nevada-gaming-control-board-asks-court-to-hold-kalshi-in-contempt.pdf)). According to [InGame's report on the filing](https://www.ingame.com/nevada-lawsuit-kalshi-geofence-penalties/), the state said Kalshi's in-house tool relied on the visitor's IP address, which a VPN gets around. It also said internet providers aren't required to assign addresses by state, and that mobile addresses aren't fixed to one place, so blocking one state by IP can be unreliable with no VPN at all. It cited one estimate putting the success rate of state-level IP blocking at 55% to 80%, and its investigators bought contracts from inside Nevada without a VPN. Kalshi had spent $190,000 on the tool and used "family and friends" to test it.

In July, Kalshi agreed to switch to a third-party geolocation provider by August 12 or pay $120,000 a day ([InGame](https://www.ingame.com/nevada-shut-down-kalshi-geofence/)). On August 15 the Board told the court that investigators had still bought banned contracts from Nevada on their phones. Kalshi answered that they had used a previous version of its app, one not subject to geofencing, and had misrepresented their residence ([The Nevada Independent](https://thenevadaindependent.com/article/control-board-kalshi-should-pay-120k-per-day-for-noncompliance-with-the-court)). A Washington court has also ordered Kalshi to geofence that state, by September 2 ([OPB](https://www.opb.org/article/2026/08/14/kalshi-ordered-sharply-curtail-operations-washington-state/)), and Michigan's order requires a third-party geolocation provider licensed by its gaming board ([InGame](https://www.ingame.com/kalshi-sports-contracts-banned-michigan/)).

The same problem comes with sweepstakes, fantasy sports, state-licensed content and promotions limited to one state.

## Every state holds VPN and proxy exits

Geolocation reports where an address is. For a VPN server or a residential proxy exit, that's where the server or the exit is, and the visitor can be anywhere. From our data on October 19, 2026, each address placed in a state by IP geolocation:

| | VPN addresses | VPN services | Residential proxy addresses, last 30 days |
|---|---:|---:|---:|
| United States | 2,747,189 | 359 | 2,304,935 |
| California | 427,228 | 234 | 236,215 |
| New York | 321,161 | 189 | 194,967 |
| New Jersey | 110,755 | 142 | 51,608 |
| Washington | 72,910 | 118 | 55,742 |
| Michigan | 41,070 | 37 | 43,942 |
| Nevada | 28,832 | 57 | 16,409 |
| Vermont, the fewest | 3,929 | 7 | 2,110 |

All counts are IPv4. The VPN columns are consumer VPN services; corporate VPN gateways and private relays are left out. 14 states and DC have exits from 50 or more VPN services, and 33 states and DC from 10 or more.

Both directions matter. A Nevada resident who wants past a Nevada geofence picks a server in California. An operator licensed to serve only New Jersey has the reverse problem: anyone outside the state can pick one of 110,755 VPN addresses in New Jersey. A residential proxy exit is someone's home connection, so to geolocation it looks like any other customer of that ISP, wherever that home is.

## Two checks, in code

Geolocate the address to a state, then ask whether the address is the visitor's own. If it's a VPN, a proxy, Tor or a server, the state you got is the exit's, and you don't know where the visitor is. Here's that as an Express route with [`vpndetection-express`](https://docs.vpndetection.io/integrations/express), geofencing Nevada:

```js
import express from 'express';
import { vpndetection } from 'vpndetection-express';
import { regionOf } from './geo.js'; // your IP geolocation lookup

const BLOCKED = 'US-NV';

// Each of these means the address isn't the visitor's own connection.
const NOT_THEIRS = [
    'isVpn', 'isResproxy', 'isMobproxy', 'isDcproxy', 'isPubproxy',
    'isTor', 'isRelay', 'isHosting',
];

const app = express();
app.set('trust proxy', 'loopback'); // your reverse proxy, on this machine
const check = vpndetection({ apiKey: process.env.VPNDETECTION_API_KEY });

app.post('/trade', check, (req, res) => {
    const { result, error } = req.vpndetection;
    const masked = NOT_THEIRS.filter((flag) => result?.[flag]);
    const region = regionOf(req.ip);
    const decision = decide(region, masked, error);
    const kind = error?.kind ?? null;
    console.log(JSON.stringify({ region, masked, error: kind, decision }));

    if (decision === 'allow') return res.json({ ok: true });
    if (decision === 'deny') {
        return res.status(403).json({ error: 'Not available in Nevada.' });
    }
    res.status(403).json({ error: 'Share your location to continue.' });
});

function decide(region, masked, error) {
    if (region === BLOCKED) return 'deny'; // VPN or not
    if (error) return 'verify'; // no answer is not a clean answer
    if (masked.length > 0) return 'verify'; // the location is the exit's
    if (region === 'US') return 'verify'; // country only: unknown, not out
    return 'allow';
}

app.listen(3000, '127.0.0.1');
```

`regionOf` stands for whatever IP geolocation you use. It returns an ISO 3166-2 code such as `US-NV`, or `US` alone when it knows the country but not the state. For these runs it answered from a table of the four addresses we tested, each placed by IP geolocation on October 21.

`isRelay` is there because a relay's address isn't always in the visitor's state: an iCloud Private Relay user on "Use country and time zone" can come out anywhere in their country and time zone, as the [Private Relay post](https://vpndetection.io/blog/block-icloud-private-relay) covers. `isHosting` is there because a server's address is the datacenter's.

The middleware sits on `/trade` alone, so a lookup happens only where the decision matters. `trust proxy` names the reverse proxy in front of the app; without it, `req.ip` is the proxy's address, and you'd geolocate and look up your own server. [The Express post](https://vpndetection.io/blog/detect-vpn-users-express-js#get-the-client-address-right) tests what goes wrong with `true` or a hop count instead.

We ran it on October 21, 2026, with Express 5.3.0, `vpndetection-express` 2.0.18 and Node 22.23.3, on a key on the Max plan. Each visitor reached `POST /trade` from the same machine with its address in `X-Forwarded-For`, as a reverse proxy sends it. The Mullvad server is `23.234.72.2`, in Los Angeles on [Mullvad's own server list](https://mullvad.net/en/servers); the other addresses are real ones we don't print.

| Visitor | `regionOf` | Flags set | Response |
|---|---|---|---|
| Mullvad server | `US-CA` | `isVpn`, `isHosting` | 403, share your location |
| Home broadband in Nevada | `US-NV` | none | 403, not available in Nevada |
| Home broadband in California | `US-CA` | none | 200 |
| Residential proxy exit in California | `US-CA` | `isResproxy` | 403, share your location |

The log read:

```
{"region":"US-CA","masked":["isVpn","isHosting"],"error":null,"decision":"verify"}
{"region":"US-NV","masked":[],"error":null,"decision":"deny"}
{"region":"US-CA","masked":[],"error":null,"decision":"allow"}
{"region":"US-CA","masked":["isResproxy"],"error":null,"decision":"verify"}
```

With geolocation alone, the Mullvad server and the proxy exit are two more California addresses, and both would have traded.

The Mullvad answer, in part:

```json
{
  "is_vpn": true,
  "is_hosting": true,
  "is_resproxy": false,
  "is_mobproxy": false,
  "is_pubproxy": false,
  "vpn": {
    "provider": "mullvad",
    "last_seen": "2026-10-21",
    "confidence": "high"
  }
}
```

It also carries `location` and `asn`, and `location` stops at the country, the United States here: the state still comes from `regionOf`.

## Unknown is not outside

`decide` turns three kinds of not knowing into `verify`, never `allow`.

A failed lookup. The middleware fails open: when our API is unreachable, your quota is spent or the key is wrong, the request goes on with `error` set. With an invalid key, the Nevada home line was still refused, since its state alone settles it, and the other three were asked for their location:

```
{"region":"US-CA","masked":[],"error":"unauthorized","decision":"verify"}
{"region":"US-NV","masked":[],"error":"unauthorized","decision":"deny"}
{"region":"US-CA","masked":[],"error":"unauthorized","decision":"verify"}
{"region":"US-CA","masked":[],"error":"unauthorized","decision":"verify"}
```

A country with no state. Some geolocation answers stop at the country. An address that might be anywhere in the US might be in Nevada, so it verifies. The IP geolocation we used placed every US IPv4 range in a state on October 19; check what yours does.

A flag your plan doesn't include. `isVpn`, `isTor` and `isRelay` come back with a key or without one, and every paid plan adds `isHosting` and the proxy flags ([pricing](https://vpndetection.io/pricing)). A flag outside your plan is `undefined`, which this code reads as not set. Without a key, the Mullvad server was still asked for its location on `isVpn`, and the California proxy exit traded:

```
{"region":"US-CA","masked":["isVpn"],"error":null,"decision":"verify"}
{"region":"US-NV","masked":[],"error":null,"decision":"deny"}
{"region":"US-CA","masked":[],"error":null,"decision":"allow"}
{"region":"US-CA","masked":[],"error":null,"decision":"allow"}
```

A geofence that has to catch residential proxies needs the plan that answers `isResproxy`. The middleware keeps each answer for an hour, so an address that reaches `/trade` costs one lookup an hour, however many requests it sends. Past a few million visitors a day, look addresses up locally in the [VPN IP database](https://vpndetection.io/databases/vpn-ip) and the [Residential Proxy IP database](https://vpndetection.io/databases/residential-proxy-ip) instead of calling the [API](https://vpndetection.io/api).

## Where IP stops

None of this knows where the visitor is. It knows when the address can't say, and Nevada's own investigators showed the other half: ordinary connections inside the state got through Kalshi's IP-only tool. And as the state told the court, a phone's address isn't fixed to one place.

What "share your location" leads to is your call. For a sweepstakes or a promotion, the browser's location prompt may be enough. For anything licensed, it's a geolocation provider your regulator accepts: Kalshi's July agreement with Nevada called for "a robust, multi-source geofencing solution provided by a third-party commercial provider", and Michigan's order requires one licensed by its gaming board. The IP check is the screen in front of it: it decides who can go straight through, and who has to prove where they are.

The same check for Utah's age verification law is in [Utah SB 73: what a site can and can't know about VPN users](https://vpndetection.io/blog/utah-sb-73-vpn).

