The Problem: AI Agents Get Banned Mid-Session

You built an AI agent that logs into a website, browses a few pages, and extracts data. It works perfectly in testing. Then you deploy it at scale and the bans start.

The root cause is almost always IP rotation during an active session. Here is what happens:

  1. Your agent starts a session with IP 103.87.91.42 (a mobile phone in India)
  2. The website sets a session cookie tied to that IP
  3. Your next request rotates to IP 103.87.91.42 (a different phone in Bangalore)
  4. The website sees a new IP on an existing session and flags it as suspicious
  5. Account locked, CAPTCHA triggered, or silent ban applied

This is the #1 reason AI agents fail on login-protected sites. The fix is sticky proxies.

What Is a Sticky Proxy?

A sticky proxy routes all your traffic through the same mobile phone every time. Instead of rotating IPs across the pool, your connection always exits through one device.

Feature Rotating Proxy Sticky Proxy
IP behavior Different IP each request Same IP every request
Use case Anonymous scraping, one-shot requests Login, multi-page flows, session state
API key Your account API key Per-node API key (one per phone)
Concurrency Spread across pool 5 connections per node
Bandwidth Per-GB billing Unlimited (flat rate)
Best for Data collection at scale AI agents, browsers, stateful workflows

How Sticky Proxies Work on Snowpad

Each sticky node is a real phone running frpc (a reverse proxy client) that tunnels its mobile data connection to our gateway. When you connect with a sticky API key:

Your Agent → gw.snowpad.io:9999 → Gateway looks up node_api_key → Routes directly to one specific phone → Phone's mobile IP exits to the target website

The gateway does no rotation. It routes directly to the assigned phone. This means:

  • Same IP every time — The phone's mobile IP is your exit IP
  • Same geographic location — The phone stays in the same city/region/carrier
  • No per-GB billing — Sticky plans are flat-rate with unlimited bandwidth
  • 5 concurrent connections per node — The phone can handle 5 simultaneous TCP connections

Getting Your Sticky Credentials

On the Snowpad dashboard, go to Proxies. You will see a Sticky Credentials section with one card per assigned node:

Sticky API Key:  YOUR_KEY
SOCKS5 URL:      socks5://YOUR_KEY:x@gw.snowpad.io:9999
Status:          online
Location:        Rishikesh, Uttarakhand

Each sticky node has its own unique API key. Use this key as the SOCKS5 username to always route through that specific phone.

Quick Start: Python

Install the SOCKS5 library:

pip install requests[socks]

Sticky IP — Same IP Every Request

import requests

STICKY_KEY = "YOUR_KEY"
PROXY = f"socks5://{STICKY_KEY}:x@gw.snowpad.io:9999"

# Every request exits through the same mobile phone
r1 = requests.get("https://httpbin.org/ip", proxies={"https": PROXY}, timeout=20)
print(r1.json())  # {"origin": "103.87.91.42"}

r2 = requests.get("https://httpbin.org/ip", proxies={"https": PROXY}, timeout=20)
print(r2.json())  # {"origin": "103.87.91.42"} — same IP!

r3 = requests.get("https://httpbin.org/ip", proxies={"https": PROXY}, timeout=20)
print(r3.json())  # {"origin": "103.87.91.42"} — still the same

Login + Browse Workflow (Session Persistence)

import requests

STICKY_KEY = "YOUR_STICKY_API_KEY_HERE"
PROXY = f"socks5://{STICKY_KEY}:x@gw.snowpad.io:9999"

session = requests.Session()
session.proxies = {"http": PROXY, "https": PROXY}

# Step 1: Login — all requests share the same IP
login = session.post("https://example.com/login", data={
    "email": "user@example.com",
    "password": "secret"
}, timeout=20)

# Step 2: Browse — session cookie + same IP
dashboard = session.get("https://example.com/dashboard", timeout=20)

# Step 3: Extract data — still same IP
data = session.get("https://example.com/api/data", timeout=20)

print(f"Login: {login.status_code}, Dashboard: {dashboard.status_code}")

Concurrent Requests (Stay Under 5 Per Node)

import requests
from concurrent.futures import ThreadPoolExecutor, as_completed

STICKY_KEY = "YOUR_STICKY_API_KEY_HERE"
PROXY = f"socks5://{STICKY_KEY}:x@gw.snowpad.io:9999"

# MAX 5 concurrent connections per sticky node
# Going over 5 gets you error 0x02 (capacity reached)
MAX_CONCURRENT = 5

urls = [f"https://httpbin.org/get?page={i}" for i in range(20)]

def fetch(url):
    return requests.get(url, proxies={"https": PROXY}, timeout=20)

# Process in batches of 5
results = []
for i in range(0, len(urls), MAX_CONCURRENT):
    batch = urls[i:i+MAX_CONCURRENT]
    with ThreadPoolExecutor(max_workers=MAX_CONCURRENT) as executor:
        futures = {executor.submit(fetch, url): url for url in batch}
        for future in as_completed(futures):
            try:
                resp = future.result()
                results.append({"url": futures[future], "status": resp.status_code})
            except Exception as e:
                results.append({"url": futures[future], "error": str(e)})
    print(f"Batch {i//MAX_CONCURRENT + 1}: {len([r for r in results if 'status' in r])} success")

Quick Start: Playwright (Node.js)

import { chromium } from 'playwright';

const STICKY_KEY = 'YOUR_STICKY_API_KEY_HERE';

const browser = await chromium.launch({
  proxy: {
    server: 'socks5://gw.snowpad.io:9999',
    username: STICKY_KEY,
    password: 'x',
  },
});

const page = await browser.newPage();
await page.goto('https://httpbin.org/ip');
const ip = await page.evaluate(() => document.body.innerText);
console.log(ip); // {"origin": "103.87.91.42"}

// Navigate to login — same IP maintained throughout the browser session
await page.goto('https://example.com/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'secret');
await page.click('#login-button');
await page.waitForNavigation();

// Continue browsing — still the same IP
await page.goto('https://example.com/dashboard');
// ...extract data...

await browser.close();

Quick Start: cURL

# Test sticky connection — same IP every time
STICKY_KEY="YOUR_STICKY_API_KEY_HERE"

curl -x socks5://${STICKY_KEY}:x@gw.snowpad.io:9999 https://httpbin.org/ip
# {"origin": "103.87.91.42"}

curl -x socks5://${STICKY_KEY}:x@gw.snowpad.io:9999 https://httpbin.org/ip
# {"origin": "103.87.91.42"} — same IP

# HTTP CONNECT proxy (alternative port)
curl -x http://${STICKY_KEY}:x@gw.snowpad.io:8443 https://httpbin.org/ip
# {"origin": "103.87.91.42"}

Error Handling: Every Error Code and What to Do

When the gateway rejects your connection, it returns a SOCKS5 reply code. Here is what each one means and how to handle it:

0x02 — Connection Not Allowed by Ruleset

This is the most common error. It means the gateway rejected your request based on policy.

Cause Log Reason Fix
Invalid or inactive API key unauthorized Check your API key. Ensure your account is active and paid.
Too many concurrent connections proxy_limit or sticky_capacity Max 5 concurrent connections per sticky node. Reduce parallel requests.
No proxies available no_proxies Your sticky node is offline. Wait and retry, or contact support.
import requests
import time

STICKY_KEY = "YOUR_STICKY_API_KEY_HERE"
PROXY = f"socks5://{STICKY_KEY}:x@gw.snowpad.io:9999"

MAX_RETRIES = 3
RETRY_DELAYS = [2, 5, 10]  # Exponential backoff

def fetch_with_retry(url):
    for attempt in range(MAX_RETRIES):
        try:
            resp = requests.get(url, proxies={"https": PROXY}, timeout=20)
            return resp
        except requests.exceptions.ProxyError as e:
            if "0x02" in str(e):
                if attempt < MAX_RETRIES - 1:
                    delay = RETRY_DELAYS[attempt]
                    print(f"Capacity reached (0x02), retrying in {delay}s...")
                    time.sleep(delay)
                    continue
                raise Exception(f"Sticky node at capacity after {MAX_RETRIES} retries")
            raise
    raise Exception("Max retries exceeded")

0x05 — Connection Refused (Upstream Failed)

The sticky phone's mobile data cannot reach the target website. This is a network issue on the phone side.

Cause Log Reason Fix
Phone lost mobile data sticky_upstream_failed Wait 5-10 seconds and retry. The phone reconnects automatically.
Target website is down sticky_upstream_failed Try a different target URL to confirm it is the phone, not the site.
Phone is offline sticky_upstream_failed Check the node status on the dashboard. If offline for more than a minute, contact support.
def fetch_with_upstream_retry(url, max_retries=3):
    """Retry on upstream failures with exponential backoff."""
    for attempt in range(max_retries):
        try:
            resp = requests.get(url, proxies={"https": PROXY}, timeout=20)
            return resp
        except requests.exceptions.ProxyError as e:
            if "0x05" in str(e):
                delay = 2 ** attempt  # 1s, 2s, 4s
                print(f"Upstream failed (0x05), retrying in {delay}s... (attempt {attempt+1}/{max_retries})")
                time.sleep(delay)
                continue
            raise
        except requests.exceptions.ConnectionError:
            # Network issue, retry
            time.sleep(2 ** attempt)
            continue
    raise Exception(f"Upstream failed after {max_retries} retries")

0x01 — General SOCKS Server Failure

This means the SOCKS5 handshake itself failed. Common causes:

  • Connection timeout during handshake (10-second limit)
  • Invalid SOCKS5 client implementation
  • Network connectivity issues between your server and the gateway
# Increase timeout and use a proper SOCKS5 library
import socks
import socket
import ssl

def sticky_request(host, port, sticky_key, target_host, target_port=443, timeout=20):
    """Raw SOCKS5 connection with proper error handling."""
    s = socks.socksocket()
    s.set_proxy(socks.SOCKS5, host, port, username=sticky_key, password="x")
    s.settimeout(timeout)
    try:
        s.connect((target_host, target_port))
        # Wrap in TLS for HTTPS
        ctx = ssl.create_default_context()
        ctx.check_hostname = True
        ctx.verify_mode = ssl.CERT_REQUIRED
        s = ctx.wrap_socket(s, server_hostname=target_host)
        return s
    except socks.GeneralProxyError as e:
        error_str = str(e)
        if "0x02" in error_str:
            raise Exception("Sticky node at capacity (5 concurrent max). Reduce parallel requests.")
        elif "0x05" in error_str:
            raise Exception("Upstream failed. Phone may have lost connectivity. Retry in 5-10 seconds.")
        elif "0x01" in error_str:
            raise Exception("SOCKS5 handshake failed. Check network connectivity.")
        raise
    except socket.timeout:
        raise Exception("Connection timed out. The phone may be offline or the target site is slow.")

Best Practices for AI Agents

1. Use Sticky for Login-Protected Sites, Rotating for Bulk Collection

Login flow:     Sticky proxy (same IP across login, browse, extract)
Data scraping:  Rotating proxy (different IP per request to avoid rate limits)

If your agent logs in and then scrapes, use a sticky proxy for the login and session, then switch to rotating for bulk data collection.

2. Never Exceed 5 Concurrent Connections Per Sticky Node

Each phone can handle 5 simultaneous TCP connections. If you exceed this, new connections get error 0x02 (capacity reached).

# BAD: 10 concurrent requests to same sticky node → 5 will fail
with ThreadPoolExecutor(max_workers=10) as executor:
    futures = [executor.submit(fetch, url) for url in urls]

# GOOD: Batch in groups of 5
for batch in chunked(urls, 5):
    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = [executor.submit(fetch, url) for url in batch]

3. Implement Exponential Backoff on 0x05 Errors

Mobile data can briefly drop. The phone reconnects automatically within seconds. Do not give up after one failure.

def fetch_with_backoff(url, max_retries=3):
    for i in range(max_retries):
        try:
            return requests.get(url, proxies={"https": PROXY}, timeout=20)
        except requests.exceptions.ProxyError:
            time.sleep(min(2 ** i, 10))  # 1s, 2s, 4s, capped at 10s
    raise Exception(f"Failed after {max_retries} retries")

4. Set Timeouts Generously

Mobile proxies add ~500-800ms latency per hop. Set timeouts to at least 20 seconds for web requests.

# BAD: 5-second timeout will fail frequently on mobile proxies
requests.get(url, proxies={"https": PROXY}, timeout=5)

# GOOD: 20-second timeout accounts for mobile network variability
requests.get(url, proxies={"https": PROXY}, timeout=20)

5. Verify Your Sticky IP Before Starting a Workflow

def verify_sticky_ip():
    """Confirm sticky proxy is working and get the exit IP."""
    try:
        r = requests.get("https://api.ipify.org/", proxies={"https": PROXY}, timeout=20)
        ip = r.json()["ip"]
        print(f"Sticky IP confirmed: {ip}")
        return ip
    except Exception as e:
        print(f"Sticky proxy not available: {e}")
        return None

6. Handle Node Offline Gracefully

Sticky phones can go offline briefly (network switches, signal drops). Your agent should detect this and retry rather than crashing.

import time

def fetch_with_node_offline_handling(url, max_retries=5, base_delay=3):
    """Handle temporary node offline with increasing delays."""
    for attempt in range(max_retries):
        try:
            resp = requests.get(url, proxies={"https": PROXY}, timeout=20)
            return resp
        except requests.exceptions.ProxyError as e:
            if "0x05" in str(e):
                delay = base_delay * (2 ** attempt)  # 3s, 6s, 12s, 24s, 48s
                print(f"Node may be offline (0x05). Waiting {delay}s before retry {attempt+1}/{max_retries}")
                time.sleep(delay)
                continue
            raise
        except requests.exceptions.ConnectionError:
            delay = base_delay * (2 ** attempt)
            print(f"Connection failed. Waiting {delay}s before retry {attempt+1}/{max_retries}")
            time.sleep(delay)
            continue
    raise Exception(f"Node offline after {max_retries} retries. Check dashboard status.")

7. Don't Reuse HTTP Sessions Across Different Sticky Nodes

Each sticky node has a different IP. If you switch nodes mid-session, the target site will see a different IP on an existing session — exactly the problem sticky proxies solve.

# BAD: Reusing session with different sticky keys
session = requests.Session()
session.proxies = {"https": f"socks5://{KEY_1}:x@gw.snowpad.io:9999"}
session.get("https://example.com/login")  # IP: 103.87.91.42

session.proxies = {"https": f"socks5://{KEY_2}:x@gw.snowpad.io:9999"}
session.get("https://example.com/dashboard")  # IP: 103.87.91.42 — different IP!

# GOOD: New session for each sticky node
def create_sticky_session(sticky_key):
    session = requests.Session()
    session.proxies = {
        "http": f"socks5://{sticky_key}:x@gw.snowpad.io:9999",
        "https": f"socks5://{sticky_key}:x@gw.snowpad.io:9999",
    }
    return session

Troubleshooting Cheat Sheet

Symptom Error Cause Fix
0x02 immediately Connection not allowed Invalid/inactive API key Check dashboard → Proxies → Sticky Credentials
0x02 after several requests Connection not allowed > 5 concurrent connections Reduce max_workers to ≤ 5 per sticky key
0x05 on every request Connection refused Sticky node is offline Check node status on dashboard. Wait 1-2 minutes.
0x05 intermittently Connection refused Phone lost mobile data briefly Retry with exponential backoff (1s, 2s, 4s)
0x01 timeout SOCKS server failure Network issue or handshake timeout Increase timeout to 20s. Check your VPS can reach gw.snowpad.io:9999.
IP changes between requests Using account API key instead of sticky key Use the per-node node_api_key, not the account API key
Very slow (>5s) Mobile network latency spike Vi (Vodafone Idea) network has occasional spikes. Use retry logic.
Dashboard shows node offline Phone app stopped or lost connectivity Restart the phone app. Check mobile data is on.

HTTP CONNECT Alternative (Port 8443)

If SOCKS5 port 9999 is blocked on your network, use HTTP CONNECT on port 8443:

# HTTP CONNECT proxy (alternative to SOCKS5)
PROXY_HTTP = f"http://{STICKY_KEY}:x@gw.snowpad.io:8443"

r = requests.get("https://httpbin.org/ip", proxies={"https": PROXY_HTTP}, timeout=20)

Both ports route through the same sticky node. Use whichever your network allows.