npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

n8n-nodes-blanktrail

v0.2.4

Published

Send HTTP requests through BlankTrail Proxy with a real browser network profile (TLS/JA3, HTTP/2, HTTP/3, headers).

Downloads

431

Readme

n8n-nodes-blanktrail

Send HTTP requests from n8n through BlankTrail Proxy, so they carry the network profile of a real browser — TLS (JA3/JA4), HTTP/2, HTTP/3, header order and connection parameters agreed as one configuration — instead of the Node.js profile that the built-in HTTP Request node cannot hide.

Switching an existing workflow is meant to be the replacement of one node: the output shape (statusCode, headers, body) matches the built-in HTTP Request.

Install

In n8n: Settings → Community Nodes → Install → n8n-nodes-blanktrail.

You also need BlankTrail Proxy running and reachable from n8n. The quickest way is the container: see deploy/docker/README.md in the product distribution.

Credential

| Field | What it is | |---|---| | Base URL | Control API. http://blanktrail:8891 inside Docker Compose, http://127.0.0.1:8891 on a desktop install. | | API Key | BT_API_KEY, or the key shown in the dashboard. | | Proxy User | User name for the proxy port itself. Default blanktrail. | | Proxy Password | BT_PROXY_PASSWORD. |

The proxy password cannot be fetched automatically. The control API deliberately returns the proxy user name and never the password, so it has to be typed here. That is a decision of the product, not a gap in this node.

The credential's Test button calls /api/v1/license/status rather than /health: health answers without authentication, so a wrong API key would pass the test and only surface later, inside somebody's workflow.

Changing the connection later

Once a credential is attached to a node, the Create new credential button on that node is gone: the node shows a Credential to connect with dropdown with a pencil beside it instead. The pencil opens the saved credential for editing, and the dropdown still offers Create new. Nothing is broken — the button disappears precisely because a credential is already bound. Every credential is also listed under Overview → Credentials, which is where to go when the node is not open in front of you.

Two changes on the BlankTrail side make a saved credential stop working.

The container was reinstalled, and the key changed with it

The control API key lives in the /data volume, not in the container image or in the environment. It is seeded once, when /data is first created, and read from /data/auth.json afterwards — which is why editing BT_API_KEY on an existing deployment changes nothing at all.

Recreate that volume — docker compose down -v, a wiped volume, a fresh host — and the key is seeded from scratch. If BT_API_KEY is set in your compose file, the same value comes back and the saved credential keeps working. If it is empty, the product generates the key itself, so the new one is different and n8n goes on sending the old one.

Read the new key out of the installation:

docker compose logs blanktrail | grep "initial API key"

It is also written to /data/initial-credentials.txt. Paste it into the API Key field of the existing credential rather than creating a second one: every node already pointing at that credential is then fixed at once.

The installation moved to another server

Then it is Base URL that changes. The address is resolved from inside the n8n container, not from your desktop: under Docker Compose it is the service name of the proxy, http://blanktrail:8891. http://127.0.0.1:8891 only works where n8n and the proxy share a network namespace, such as a desktop install — inside the n8n container, localhost is n8n itself.

What a stale connection looks like

| Message | What it means | |---|---| | cannot reach the installation at … — the connection was refused (ECONNREFUSED) | The address is shaped right, but nothing answers on it: the container is not running, or the control API port is not published to n8n. | | cannot reach the installation at … — the host name does not resolve (ENOTFOUND) | The name in Base URL means nothing on the network n8n sits in: the installation moved, or the name is not the Compose service name. | | the control API rejected the key | The installation answers, but the API Key is not its key — typically the /data volume was recreated and the key with it. |

The first two name the address they tried, so the message itself says which credential field to look at.

How it differs from the built-in HTTP Request

| | Built-in | This node | |---|---|---| | TLS fingerprint | Node.js | selected browser | | HTTP/2 · HTTP/3 | Node.js defaults | browser-shaped | | Header order | library order | browser order | | Certificate of the proxy | needs NODE_EXTRA_CA_CERTS | fetched over the API and used directly | | Proxy credentials | manual | from the credential |

Because the node fetches the CA itself, it works without NODE_EXTRA_CA_CERTS. Set that variable anyway if other nodes in the same workflow talk through the proxy.

Never switch on "Ignore SSL issues" to make things work: it disables certificate checking entirely, including for genuine external certificates.

Ports and profiles

The node ensures a port rather than opening one: if the port is already open, it is used as it is and its configuration is not touched.

The profile belongs to the port, not to the request. Two executions running at the same time and asking for different browsers would otherwise reconfigure the one port under each other, and the request would quietly leave with somebody else's fingerprint. So Browser and Operating System apply only when this node is the one that opens the port.

The same rule covers Upstream Proxy and Chain Proxy, added for the same reason. Upstream Proxy is the exit proxy the port's traffic leaves through — SOCKS5 or HTTP, credentials included in the URL if it needs them (socks5://user:pass@host:1080); empty means a direct connection. Chain Proxy is an extra first hop placed before it (http://host:3128); empty means none. Both are read only when this node opens the port, exactly like Browser and Operating System.

On the single-thread plan there is one port. Asking for a second one fails with a message saying the limit is reached — close a port you no longer use, or raise the plan.

The BlankTrail node

Besides replacing the HTTP Request node, the package ships a second node — BlankTrail — for the control API itself: opening and closing ports, listing fingerprint profiles, and reading installation status. Where the HTTP Request node opens a port only as a side effect of sending a request, this node exposes each operation directly, so a workflow can prepare a port, inspect it, or tear it down as its own step.

| Resource | Operation | What it does | |---|---|---| | Port | Open | Open a port | | Port | Close | Close a port | | Port | List | List open ports | | Port | Get | Get the configuration of a port | | Port | Suggest | Suggest a free port number | | Profile | List | List available profiles (one page; Browser and Limit narrow it, total says how many there are) | | Profile | Get Applied | Get the profile applied to a port | | Status | Health | Check that the installation responds | | Status | License | Get the license status | | Status | Solver | Get Challenge Breaker capacity |

Example: open a Chrome/Windows port, then request through it

  1. BlankTrail — Resource Port, Operation Open, Port 10001, and under Options set Browser to chrome, Operating System to windows and Spoof Headers to true. The output carries current_profile, the fingerprint the port actually got, so the run can be checked rather than assumed.
  2. BlankTrail HTTP Request — Port 10001 (or {{ $('BlankTrail').item.json.port }}, which also works when step 1 asked the server for a free port), Method and URL as needed, and under Headers a User-Agent — any value, {"User-Agent": "n8n"} will do. The port is already open with the profile from step 1, and an already-open port is used exactly as it is — so this second node just sends the request through it, replacing that User-Agent with the profile's own.

Spoofing rewrites the User-Agent, it does not add one

Both spoofing options are off by default, matching the product, and both only rewrite a User-Agent that the request actually sends. Neither adds a missing one. That is why the two halves of the example have to be set together: with no User-Agent in the request there is nothing to rewrite, and the request leaves carrying the TLS and HTTP/2 of a real Chrome and no User-Agent at all — a combination that stands out more than an honest Node.js one.

  • Spoof User Agent rewrites that one header.
  • Spoof Headers sends the browser's whole header set in its order, and rewrites the User-Agent as well — so it covers Spoof User Agent, and one of the two is enough.

Both are settings of the port, so they live on the Open step and, like Browser and Operating System, apply only to the run that actually opens it.

Opening the port as its own step is only needed when a workflow reuses one port across several HTTP Request nodes, or inspects it (Get, Profile → Get Applied) before sending anything. A single BlankTrail HTTP Request node already opens a port on its own when neither is needed.

Idle ports close themselves. Open takes an Idle Seconds option, default 1800 (30 minutes); after that much inactivity the installation closes the port on its own, and 0 turns this off. This is not a convenience knob — a scenario that opens a port and then fails before reaching Close would otherwise leave it standing forever, and on the single-thread plan the port limit is exactly the kind of thing that goes unnoticed for a hundred runs and then fails abruptly: the error surfaces on a later, unrelated execution that merely asked for a free port, not on the run that actually leaked it.

Open is idempotent: a port that is already open is returned as it is, with its current configuration, and is not reconfigured. Re-running the workflow therefore does not fail with "port already open", and on a one-port plan it does not report the limit as reached against the very port it asked for. The flip side is that Browser, Operating System and Specific Profile apply only to the run that actually opens the port — to change the profile of a standing port, close it first.

Both runs return the same shape, so an expression written against the first one keeps working on the second: port, protocol, status and current_profile (name, browser, os, user_agent) are always there. status is the one value that differs — opened on the run that opened the port, already_open on a run that found it standing — which is how a workflow can tell the two apart. The idempotent run also passes the port's stored configuration through, so the output additionally carries fields such as mode and browser. Beware that this top-level browser is the filter the port was opened with, not the browser it ended up with — the applied one is current_profile.browser, and on an unlucky pick the two genuinely differ.

The two nodes disagree about how long a port lives — read this before scheduling

Open arms an auto-close (Idle Seconds, 1800 by default). The HTTP Request node does not: it ensures the port, so when the idle timer has already closed it, that node silently re-opens it — without the Browser and Operating System filters set on the Open step, because those live on the Open node, not on the request. A scheduled workflow then keeps working and quietly leaves with a different fingerprint than the one it was built around, with nothing in the output to say so.

Two ways to avoid it, pick one:

  • Repeat Browser and Operating System on the BlankTrail HTTP Request node as well, so a re-opened port comes back with the same profile; or
  • set Idle Seconds: 0 on Open, so the port never closes itself, and close it explicitly with Port → Close at the end of the run.

The second one trades the leak the idle timer prevents for a stable fingerprint — use it only where a Close step is guaranteed to run.

The same goes for the two spoofing options, and only the second way covers them: the HTTP Request node has no Spoof Headers field to repeat, so a port it re-opens comes back with spoofing off and the User-Agent written in Headers goes out untouched, next to a browser TLS fingerprint. Where the spoofing matters, pin the port with Idle Seconds: 0 and close it explicitly.

SOCKS5 ports and the HTTP Request node

Open can create a socks5 port, but the BlankTrail HTTP Request node cannot send through one: it builds an HTTP proxy agent. Use http for ports that node will use, and reach for socks5 only when some other client (a browser, a scraper, another tool) connects to the port directly.

Licence

MIT