ChangeMyIP on Linux: Advanced Per-Country Rank Tracking Pipeline
Build a scheduled, per-market rank-checking pipeline on Linux that rotates ChangeMyIP egress IPs, verifies the exit country on every check, and logs diffable results you can trust.
Overview
Rank tracking is only as trustworthy as the IP you query from. When every check in a multi-market campaign leaves through one address, search and analytics endpoints can return cached, personalised, or throttled responses, and the numbers you report stop reflecting what a local visitor actually sees.
This tutorial builds an advanced, scheduled rank-checking pipeline on Linux that uses ChangeMyIP egress IPs per market. You will:
- confirm the proxy endpoint and exit identity before each batch,
- send requests over HTTP/HTTPS or SOCKS5 with retry, jitter, and timeout handling,
- reject responses that did not exit from the expected country,
- append results to a diffable JSONL log and schedule the job with cron,
- keep credentials outside your scripts and out of version control.
ChangeMyIP provides dedicated and shared datacenter proxies and rotating residential proxies over HTTP, SOCKS5, Shadowsocks, Trojan, DNS, and WireGuard, spanning 40+ countries, 300+ cities, and 1000+ subnets on flat monthly billing, which makes it a practical egress layer for a recurring, market-by-market crawler.
Prerequisites
- A Linux host with outbound network access (Debian/Ubuntu, Fedora, or similar).
- Python 3.9 or newer with the
requestslibrary. curlandjqfor quick manual verification.- An active ChangeMyIP subscription, plus the gateway host, protocol port, username, and password shown in your dashboard.
- A list of target markets (for example
us,gb,de,jp,br) and the pages you want to track.
Choosing Between Datacenter and Residential Egress
- Datacenter (dedicated or shared) — the fastest and most economical option per check. Use it for high-volume secondary checks, sitemap polling, and markets where result personalisation is not the point.
- Residential (rotating) — consumer ISP addresses. Use it when a page or result set must look like a genuine local visitor, and when datacenter ranges are challenged or answered with degraded content.
- Mixed routing — send geo-sensitive markets through residential egress and keep the long tail on datacenter. Because ChangeMyIP bills flat monthly, adding scheduled volume is easy to budget for.
Whatever you choose, verify the exit country rather than assuming it.
Steps
1. Collect endpoint details and store them securely
Sign in to the ChangeMyIP dashboard and copy the gateway host, the port for the protocol you will use (HTTP/HTTPS or SOCKS5), your username, and your password. Keep them in a root-readable environment file rather than inside your scripts:
sudo install -d -m 700 /etc/cmip
sudo tee /etc/cmip/env >/dev/null <<'EOF'
CMIP_HOST=gateway-host-from-dashboard
CMIP_PORT=port-from-dashboard
CMIP_USER=your-username
CMIP_PASS=your-password
CMIP_SCHEME=http
EOF
sudo chmod 600 /etc/cmip/env
Set CMIP_SCHEME to socks5h when the port you were issued speaks SOCKS5 and you want DNS resolved at the proxy instead of on your host.
2. Install the toolchain
sudo apt update
sudo apt install -y curl jq python3 python3-pip
python3 -m pip install --user requests
3. Verify a single request through the proxy
set -a; . /etc/cmip/env; set +a
curl -sS --max-time 20 \
--proxy "${CMIP_SCHEME}://${CMIP_USER}:${CMIP_PASS}@${CMIP_HOST}:${CMIP_PORT}" \
https://ipinfo.io/json | jq '{ip, city, region, country, org}'
Run it three or four times. On a rotating plan the exit IP should change between runs, and it should never match your own ISP address. If the output is your own connection, the proxy is not being applied at all.
4. Describe your markets and targets in one file
{
"markets": {
"us": ["https://example.com/pricing"],
"gb": ["https://example.com/pricing"],
"de": ["https://example.com/pricing"]
}
}
Save it as /etc/cmip/targets.json and set the market codes to match the country endpoints available in your ChangeMyIP dashboard.
5. Write the pipeline with per-check geo verification
#!/usr/bin/env python3
'''Per-market rank checks through ChangeMyIP rotating proxies.'''
import json
import os
import random
import time
from pathlib import Path
import requests
HOST = os.environ['CMIP_HOST']
PORT = os.environ['CMIP_PORT']
USER = os.environ['CMIP_USER']
PASS = os.environ['CMIP_PASS']
SCHEME = os.environ.get('CMIP_SCHEME', 'http')
TARGETS = json.loads(Path('/etc/cmip/targets.json').read_text())
OUT = Path('/var/log/cmip/rank-checks.jsonl')
OUT.parent.mkdir(parents=True, exist_ok=True)
def proxy_url():
return f'{SCHEME}://{USER}:{PASS}@{HOST}:{PORT}'
def get(session, url, retries=4):
last = None
for attempt in range(1, retries + 1):
try:
resp = session.get(url, timeout=25)
resp.raise_for_status()
return resp.text
except requests.RequestException as exc:
last = exc
time.sleep(min(2 ** attempt + random.random(), 30))
raise last
def run():
for market, urls in TARGETS['markets'].items():
session = requests.Session()
session.proxies = {'http': proxy_url(), 'https': proxy_url()}
session.headers['User-Agent'] = (
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/124.0 Safari/537.36'
)
try:
geo = json.loads(get(session, 'https://ipinfo.io/json'))
exit_country = (geo.get('country') or '').lower()
except (requests.RequestException, json.JSONDecodeError) as exc:
print(f'{market}: geo check failed: {exc}')
continue
for url in urls:
record = {
'ts': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
'market': market,
'url': url,
'exit_ip': geo.get('ip'),
'exit_country': exit_country,
'country_ok': exit_country == market,
}
try:
body = get(session, url)
record['bytes'] = len(body)
record['status'] = 'ok'
except requests.RequestException as exc:
record['status'] = type(exc).__name__
with OUT.open('a') as fh:
fh.write(json.dumps(record) + '\n')
print(record)
if __name__ == '__main__':
run()
Save it as /opt/cmip/rank_check.py. The country_ok flag is what keeps your reporting honest: a check that exited from the wrong country is still recorded, but you can exclude it downstream.
Because a rotating plan can hand a different exit IP to each request, the geo check and the page fetch may leave from different addresses. If your ChangeMyIP dashboard exposes session or rotation controls, such as a session length or a sticky session token, pin one exit IP per check so both requests share an address; otherwise treat country_ok as a strong signal for the batch rather than a guarantee for every individual fetch.
6. Run it once in the foreground
set -a; . /etc/cmip/env; set +a
python3 /opt/cmip/rank_check.py
Inspect the log before you schedule anything:
jq -s 'group_by(.market) | map({market: .[0].market, checks: length, ok: (map(select(.country_ok)) | length)})' /var/log/cmip/rank-checks.jsonl
7. Schedule the job
17 */6 * * * set -a; . /etc/cmip/env; set +a; /usr/bin/python3 /opt/cmip/rank_check.py >> /var/log/cmip/run.log 2>&1
Stagger minutes instead of running everything on the hour, and keep the total volume in line with what your ChangeMyIP plan is sized for.
8. Diff the history instead of re-reading single runs
jq -r 'select(.country_ok) | [.ts, .market, .url] | @tsv' /var/log/cmip/rank-checks.jsonl | tail -n 40
Verify the Exit IP for Every Market Endpoint
Repeat this block once per country endpoint shown in your ChangeMyIP dashboard, exporting the matching CMIP_SCHEME, CMIP_USER, CMIP_PASS, CMIP_HOST, and CMIP_PORT for each one:
set -a; . /etc/cmip/env; set +a
for attempt in 1 2 3; do
curl -sS --max-time 20 \
--proxy "${CMIP_SCHEME}://${CMIP_USER}:${CMIP_PASS}@${CMIP_HOST}:${CMIP_PORT}" \
https://ipinfo.io/json | jq -r '[.country, .city, .ip, .org] | @tsv'
done
Record the countries you actually get. Those are the markets you can report on with confidence.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
407 Proxy Authentication Required |
Credentials missing, mistyped, or containing characters that need URL encoding | Recheck the dashboard values and percent-encode reserved characters in the password before building the proxy URL, for example @ becomes %40 |
curl: (7) or connection timed out |
Wrong port, or an HTTP port used with a SOCKS5 scheme | Re-copy the port that matches the protocol you selected |
| Exit country is consistently not the market you asked for | Country targeting is not applied to that endpoint or port, or rotation selected another region | Use the country-specific endpoint listed in your dashboard and pin a session if that control exists |
429 or 503 from the target site |
Request pace is too aggressive | Increase the delay between requests, add jitter, and spread markets across the schedule |
| TLS handshake failures | Scheme and port mismatch | Try the socks5h scheme against the matching SOCKS5 port |
| Behaviour differs from a manual browser check | Cookies, caching, or a different user agent | Reuse one requests.Session per market and keep headers consistent between runs |
| DNS appears to resolve locally | HTTP proxy scheme resolves names on your host | Switch to socks5h so lookups happen at the proxy |
| Log file grows without bound | No rotation or retention policy | Add logrotate or prune entries older than your retention window |
Summary
- A rank check is only valid when you know which country it exited from; geo verification is not optional at scale.
- ChangeMyIP's rotating residential egress suits market-accurate checks, while dedicated or shared datacenter egress suits volume work; flat monthly billing keeps the budget predictable.
- Keep the pipeline honest by logging
country_okper record and filtering on it downstream. - Verify each country endpoint manually before trusting an automated schedule, and re-run that verification whenever you add a market.
- Escalate responsibly: steady pacing, jittered retries, and one session per market will outperform brute force every time.