On-prem
xhr.dev is on-prem only. You run the solver as a container on your own infrastructure, and it talks directly to the target sites and proxies you configure — never through infrastructure xhr.dev operates. Your traffic, cookies, and target-site data stay inside your network.
There was once a hosted proxy. There isn't now, and the only solver we run is the trial box we lend to prospects during an evaluation.
Network posture
The container is designed to run on a box with no general internet access. That isn't only a deployment recommendation — the application enforces it on itself.
The runtime runs under a restrictive permission model
The runtime starts under Node's permission model, and keeps only what a solve actually needs. Verifiably, in the shipped image, the process:
- cannot spawn a subprocess or a shell —
child_processfails withERR_ACCESS_DENIED, - cannot write to the filesystem at all, and
- can read only its own unpacked payload — not your filesystem, not your mounted secrets. Reading
/etc/passwdfails withERR_ACCESS_DENIED.
Those permissions are granted at startup by the launcher and cannot be widened by anything that runs afterwards, including the challenge script.
What this does not constrain
It does not restrict outbound network destinations, and we would rather say so than imply otherwise. The runtime is started with --allow-net=0.0.0.0:3000, which names the port it serves you on — but Node's --allow-net is a boolean and takes no address allowlist, so read that as intent rather than as enforcement. It is also one permission covering both listening and connecting: a process that serves you an API cannot give up outbound without giving up the ability to bind its own port. So the runtime either has network or it does not.
What it actually does with it is narrower than that permission suggests. A DataDome solve makes no outbound request at all: iframeData is required because the container cannot fetch the challenge document itself (omitting it returns 400), and a proxy in the body is accepted and ignored — an unroutable one returns the same prepared submission as none. The outbound path that does get used is Akamai's server-side solve, which fetches the sensor script and submits the payload to the origin unless you send submit: false — and which routes that through the proxy you name, defaulting to 127.0.0.1:8080 if you name none.
So "it never phones home" is not something a runtime flag can prove to you. What can prove it is your own network, which is why the check below is behavioural rather than a config dump. Egress rules on the host, or an --internal docker network, are what actually bound where this process can reach — and unlike a flag, you control and can audit them.
Verify it yourself, behaviourally
Don't take our word for it, and don't settle for reading a config either — make the container prove it. Run it with egress restricted to your proxy and nothing else:
# no default route to the internet; only your proxy is reachable
docker network create --internal xhrdev-net
docker run -d --name xhrdev --network xhrdev-net ... ghcr.io/xhrdev-<your-org>/xhrdev:latestSolves keep working. Then point a packet capture or your egress firewall logs at it for as long as you like: the only outbound flows are to the proxy you configured.
That's a stronger artifact for a security review than any policy document or config dump, because it's an observed property of the running system rather than a claim about it. It's also the reason this passes review at shops that can't route traffic through a vendor's proxy at all.
The sandbox has no network at all
One layer further in: the anti-bot vendor's obfuscated challenge script is untrusted code, and it runs in a sandbox whose network surfaces are patched rather than real. XMLHttpRequest, WebRTC, and Workers are reimplemented and answered in-process, so the challenge script cannot open a socket even in principle — its "requests" never leave the process, let alone the box.
What that means for your firewall
Lock the box down. The container needs:
| Direction | Destination | Why |
|---|---|---|
| inbound | port 3000, from your scrapers only | the API |
| outbound | your proxy | fetching the challenge and submitting to the target |
Nothing else — no DNS to xhr.dev, no registry access at runtime, no NTP dependency unless you've opted into time-source hardening. Egress restricted to your proxy pool is a supported, tested configuration.
The network is the only access control
There is no per-request authentication and no API key — the licence gates startup, not requests. Anything that can reach port 3000 can use the solver. That's the trade for having no auth to misconfigure, and it means reachability is your whole access-control story. See Security.
Run the container locked down
The runtime confines itself, but it runs on your infrastructure and the container boundary is yours. We ship and test against this flag set, and recommend you deploy with it:
docker run -d \
--name xhrdev \
--restart unless-stopped \
--cap-drop=ALL \
--security-opt no-new-privileges:true \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m \
--pids-limit 512 \
--memory 6g \
--cpus 3 \
--dns 1.1.1.1 --dns 8.8.8.8 \
-p 3000:3000 \
-v ./licence:/run/licence:ro \
ghcr.io/xhrdev-<your-org>/xhrdev:latest| Flag | What it buys you |
|---|---|
--cap-drop=ALL | Nothing here needs a Linux capability; it binds :3000 as an unprivileged uid. |
--security-opt no-new-privileges | Closes the setuid route to more privilege. |
--read-only | The image filesystem cannot be modified, so nothing can be left behind that survives a restart. |
--tmpfs /tmp with noexec,nosuid | Provides the scratch space the runtime needs without allowing a binary to be staged in it. |
--pids-limit, --memory, --cpus | A runaway workload degrades this container rather than the host. Size to your box — --cpus must not exceed the cores actually available. |
--dns | Keeps the container off your VPC resolver, which on AWS, GCP and OCI often shares an address with the instance metadata service. |
Two more worth doing at the network layer, which Docker flags can't express:
- Block egress to
169.254.169.254. On the major clouds that address serves instance credentials to anything that can reach it. If your resolver lives there too, allow UDP/TCP 53 and drop the rest. - Stop the container connecting to the host itself and to the rest of your private range. It only ever needs your proxy and your targets.
None of this changes how the solver behaves — every gate in our release pipeline runs against a container started this way.
Licence, not phone-home
A signed licence file unlocks the container at startup: it verifies the Ed25519 signature over your licence and checks the expires_at deadline, entirely locally. There's no licence server to call, which is why the egress-restricted deployment above works at all: run it with no route to us and licensing still functions.
If the licence is missing, expired, or tampered with, the container exits immediately with {"event":"launcher_failed"} and a non-zero code. It fails closed, not open.
See Deployment for delivery and renewal.
What it solves
- Akamai Bot Manager — sensor challenges (
_abck/bm-sz) and SBSD. Direct HTTP solve, or a streaming WebSocket session for browser-automation setups. API reference - DataDome — captcha and interstitial challenges, via a single-shot HTTP call. API reference
Don't see what you need? Message us.
The hosted trial box
The one exception to all of the above. During an evaluation we'll point you at a solver we run, so you can try the API before deploying anything. It sits behind a reverse proxy that requires an x-api-key header on /akamai/* and /dd/* (/hc stays open), and the key is issued with the trial and revoked when it ends.
That key exists because the box is on the public internet. It has no equivalent in a self-hosted deployment — your own container has no API key and ignores the header. It's also the only configuration in which any of your traffic touches a machine we operate; the moment you deploy your own container, that stops.
The OpenAPI spec carries both: the trial server with ApiKeyAuth, and your own container with no security requirement.
Next
- Deployment — pulling the image and running it
- Security — the full trust model
- How to integrate