# Ludax proxies in Scrapy: guide for AI assistants

> Give this file to your AI assistant (ChatGPT, Claude, Cursor, Copilot) and it can add Ludax proxies to a Scrapy project. Human-readable version: https://ludaxproxy.com/docs/scrapy

You are helping a developer route a Scrapy crawl through Ludax residential proxies. Ludax is a standard HTTP proxy with a username and a password: no SDK, no proxy list, no API key. Follow the rules below exactly; they come from the gateways' behaviour and from tests with Scrapy 2.20 on Python 3.14 (2026-11-10).

## Rules

- A proxy per request: `meta={"proxy": "http://resi-lx_k3m9q2p7xa:YOUR_PASSWORD@<gateway>"}`, for example `gw.ludaxproxy.com:7777`. Scrapy's HttpProxyMiddleware (priority 750) turns the credentials in the URL into the login header. Do not suggest scrapy-rotating-proxies or proxy lists: the rotation happens behind the one gateway.
- For a whole project, use the `LudaxProxyMiddleware` below at priority 610: before HttpProxyMiddleware (750) and, for errors, before RetryMiddleware (550). A proxy already in `meta` wins.
- **One connection is one IP, and Scrapy reuses connections.** For a new IP on every request give each request its own session token (`-session-<random>-time-1`); the middleware does this when `LUDAX_STICKY_MINUTES = 0`.
- **Same IP for a flow:** `-session-<TOKEN>-time-<MINUTES>` (up to 1000 minutes). With the middleware: `LUDAX_STICKY_MINUTES = 30` and `meta={"ludax_session": "account1"}` per flow. Tokens are 3 to 24 letters or digits, no dashes.
- **Errors:** a refused login on an https:// URL is `TunnelError: Could not open CONNECT tunnel with proxy ... [{'status': 407, ...}]`, which Scrapy's RetryMiddleware retries; on an http:// URL it is a 407 response. Never let a 407 be retried: 30 failed logins in 5 minutes block the client IP for 10 minutes. The middleware closes the spider (`finish_reason: proxy_login_refused`) at the first 407. A 502 (`'status': 502`) is a dropped exit and the built-in retry is right for it.
- Spiders start with `async def start(self)` in recent Scrapy; in a test with Scrapy 2.20 a spider with only `start_requests()` sent nothing.
- **Blocked from clean IPs:** Scrapy's TLS handshake is not a browser's. Use the TLS gateways (Residential `gw.ludaxproxy.com:9999`, Premium `gw.ludaxproxy.com:8081`) with `-AE-<profile>` at the end of the username (for example `resi-lx_k3m9q2p7xa-AE-chrome154`). Scrapy does not verify certificates by default, so the re-signed HTTPS works without the CA; a project that verifies needs https://ludaxproxy.com/downloads/ludax-ca.crt. On the TLS gateways a sticky IP needs `-sameip` after the time (the middleware adds it).
- SOCKS5 is not supported by Scrapy's HttpProxyMiddleware: use the HTTP gateways.

## Credentials

- **Username:** the product prefix (`resi-` or `premi-`), then the developer's account name from the dashboard (Generate), for example `resi-lx_k3m9q2p7xa`. The account name is `lx_` and 10 characters, the same for every plan. Never invent one: ask the developer, or use a placeholder such as `YOUR_USERNAME`.
- **Password:** the plan's own password (16 hexadecimal characters, one per plan), then the options, in this order: `-session-<TOKEN>-time-<MINUTES>`, then `-country-<cc>`, `-state-<code>`, `-city-<name>` (and `-asn-<number>`, `-os-<device>` on Premium). Use a placeholder such as `YOUR_PASSWORD`, or read it from an environment variable. Never put options in the username.
- **Sessions:** without `-session-` every new connection leaves from a new IP. `-session-<TOKEN>-time-<MINUTES>` keeps one IP for every connection that sends the same token (TOKEN: 3 to 24 letters or digits; MINUTES: 1 to 1000; Premium holds a session at least 3 minutes). Always send `-time-`: without it a session lasts 1 minute. On the TLS gateways add `-sameip` after the time, or only the browser profile is held.
- **Location:** `-country-` takes a lower-case ISO code (`us`, `de`, `jp`); `-state-` and `-city-` follow it (`-country-de-state-by-city-munich`); on Premium the state is its name without spaces (`-country-us-state-illinois-city-chicago`), in any of 219 countries. `-os-` (Premium) takes `windows`, `android`, `ios` or `macos`. With no free exit in that city or region the request falls back to the country. Codes for every country: https://ludaxproxy.com/locations
- **The port picks the product:** a plan's password works only on its own product's gateway, and the prefix must match it.

## Products and gateways in force

| Product | Username prefix | HTTP and HTTPS | SOCKS5 | Browser TLS | Sticky session |
| --- | --- | --- | --- | --- | --- |
| Residential | `resi-` | `gw.ludaxproxy.com:7777` | `gw.ludaxproxy.com:7575` | No | up to 1000 min |
| Residential TLS | `resi-` | `gw.ludaxproxy.com:9999` | none | Yes | up to 1000 min |
| Residential Premium | `premi-` | `gw.ludaxproxy.com:8080` | `gw.ludaxproxy.com:1080` | No | up to 1000 min |
| Residential Premium TLS | `premi-` | `gw.ludaxproxy.com:8081` | none | Yes | up to 1000 min |

## Check the connection

`GET https://api.ludaxproxy.com/whoami` through the proxy returns JSON: `ip`, `country` (full name), `type`, `tls_profile` (the browser the TLS handshake matches) and `latency_ms`. Add `?full=1` for a `debug` block with the JA4 fingerprint, the HTTP/2 fingerprint, the ASN and the User-Agent that arrived.

## Tested examples

The same code as on https://ludaxproxy.com/docs/scrapy. Replace `lx_k3m9q2p7xa` and `YOUR_PASSWORD` with the developer's credentials.

### Install

```bash
pip install scrapy
```

### Quick test: a proxy in meta

```python
import scrapy

PROXY = "http://resi-lx_k3m9q2p7xa:YOUR_PASSWORD-country-us@gw.ludaxproxy.com:7777"


class WhoamiSpider(scrapy.Spider):
    name = "whoami"

    async def start(self):
        # the proxy goes in meta; Scrapy sends the login for you
        yield scrapy.Request("https://api.ludaxproxy.com/whoami", meta={"proxy": PROXY})

    def parse(self, response):
        yield response.json()
```

```bash
scrapy runspider whoami_spider.py
```

### The middleware for a whole project

```python
import secrets

from scrapy.core.downloader.handlers.http11 import TunnelError
from scrapy.exceptions import IgnoreRequest
from scrapy.utils.defer import deferred_from_coro


class LudaxProxyMiddleware:
    """Sends every request through Ludax and stops the crawl on a refused login."""

    def __init__(self, crawler):
        s = crawler.settings
        self.crawler = crawler
        self.user = s["LUDAX_USER"]
        self.password = s["LUDAX_PASSWORD"]
        self.gateway = s.get("LUDAX_GATEWAY", "gw.ludaxproxy.com:7777")
        self.country = s.get("LUDAX_COUNTRY")
        self.sticky_minutes = s.getint("LUDAX_STICKY_MINUTES")  # 0: a new IP per request
        self.tls = "-AE-" in self.user or "-NAE-" in self.user  # a TLS port
        self.spider_token = secrets.token_hex(4)

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def process_request(self, request):
        if "proxy" in request.meta:
            return None  # the spider chose a proxy itself
        if self.sticky_minutes:
            # one IP for the whole spider; meta["ludax_session"] gives a flow its own IP
            token = request.meta.get("ludax_session", self.spider_token)
            options = f"-session-{token}-time-{self.sticky_minutes}"
            if self.tls:
                options += "-sameip"  # on the TLS ports, hold the IP as well as the browser
        else:
            # a token of its own: a new connection, so a new IP, for every request
            options = f"-session-{secrets.token_hex(4)}-time-1"
        if self.country:
            options += f"-country-{self.country}"
        request.meta["proxy"] = f"http://{self.user}:{self.password}{options}@{self.gateway}"
        return None

    def refused(self, detail):
        # every other request would be refused the same way, and retries only
        # count towards the block (30 failed logins in 5 minutes)
        deferred_from_coro(self.crawler.engine.close_spider_async(reason="proxy_login_refused"))
        raise IgnoreRequest(f"Ludax refused the login (407): {detail}")

    def process_response(self, request, response):
        if response.status == 407:  # http:// URLs get the gateway's answer as a response
            self.refused(request.url)
        return response

    def process_exception(self, request, exception):
        if isinstance(exception, TunnelError) and "'status': 407" in str(exception):
            self.refused(exception)  # https:// URLs get it as a tunnel error
        return None
```

```python
import os

DOWNLOADER_MIDDLEWARES = {
    # before HttpProxyMiddleware (750), which turns meta["proxy"] into the login header
    "myproject.middlewares.LudaxProxyMiddleware": 610,
}

LUDAX_USER = "resi-lx_k3m9q2p7xa"
LUDAX_PASSWORD = os.environ["LUDAX_PASSWORD"]  # the plan password, kept out of the code
LUDAX_GATEWAY = "gw.ludaxproxy.com:7777"
LUDAX_COUNTRY = "us"  # None: any country
LUDAX_STICKY_MINUTES = 0  # 0: a new IP per request; 30: one IP for 30 minutes
```

```bash
export LUDAX_PASSWORD="YOUR_PASSWORD"
scrapy crawl myspider
```

### One IP per account (sticky sessions)

```python
import scrapy


class AccountsSpider(scrapy.Spider):
    """Each account keeps its own IP: run with LUDAX_STICKY_MINUTES = 30."""

    name = "accounts"

    async def start(self):
        for account in ["shop1", "shop2", "shop3"]:
            # the same ludax_session = the same IP; letters and digits only
            for page in range(3):
                yield scrapy.Request(
                    f"https://api.ludaxproxy.com/whoami?account={account}&page={page}",
                    meta={"ludax_session": account},
                    cb_kwargs={"account": account},
                    dont_filter=True,
                )

    def parse(self, response, account):
        yield {"account": account, "ip": response.json()["ip"]}
```

### Geo-targeting per request

```python
import scrapy

USER = "resi-lx_k3m9q2p7xa"
PASSWORD = "YOUR_PASSWORD"
GATEWAY = "gw.ludaxproxy.com:7777"


class PricesSpider(scrapy.Spider):
    """The same page seen from three countries."""

    name = "prices"

    async def start(self):
        for country in ["us", "de", "jp"]:
            proxy = f"http://{USER}:{PASSWORD}-country-{country}@{GATEWAY}"
            yield scrapy.Request(
                "https://api.ludaxproxy.com/whoami",
                meta={"proxy": proxy},  # a proxy in meta wins over the middleware
                cb_kwargs={"country": country},
                dont_filter=True,
            )

    def parse(self, response, country):
        yield {"asked": country, "got": response.json()["country"]}
```

### Concurrency, AutoThrottle and retries

```python
# settings.py: fast, but gentle on the site and on your GB
CONCURRENT_REQUESTS = 16
CONCURRENT_REQUESTS_PER_DOMAIN = 8
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_TARGET_CONCURRENCY = 4
DOWNLOAD_TIMEOUT = 30

# a dropped exit (502) is retried by Scrapy's own RetryMiddleware: 502 is in its
# default RETRY_HTTP_CODES, and a failed tunnel is retried as well
RETRY_TIMES = 3
```

### Browser TLS fingerprint through the TLS gateway

```python
# settings.py: the Residential TLS port, looking like Chrome 154
LUDAX_USER = "resi-lx_k3m9q2p7xa-AE-chrome154"
LUDAX_GATEWAY = "gw.ludaxproxy.com:9999"
```

## Plans and help

- Prepaid, no subscription: 1 GB of Residential Proxies costs $0.99. The smallest plan is the test; there is no free trial. Every plan: https://ludaxproxy.com/pricing
- Credentials: dashboard, Generate (https://ludaxproxy.com/dashboard). A plan's password can be reset there.
- Full reference: https://ludaxproxy.com/docs. Whole site for AI: https://ludaxproxy.com/llms.txt
- Support: Telegram https://t.me/LudaxProxy, email support@ludaxproxy.com.
