DEV Community

0xGollum
0xGollum

Posted on Edited on

Getting past Binance and Bybit's datacenter IP blocks without overpaying for proxies

This actor compares perpetual futures funding rates across seven exchanges (Binance, Bybit, OKX, Gate.io, KuCoin, Bitget, MEXC) in one run and flags arbitrage spreads between them - one clean output instead of seven tabs and a spreadsheet.

Most exchanges have clean, key-less public APIs that just work from any server, including cloud/datacenter IPs.

Binance is not one of them. Hit its public funding rate endpoint from a datacenter IP and you get a flat HTTP 451 — geo-blocked, no way around it with a direct connection.

I'd shipped without Binance for months, figuring that wall was permanent. Then, while extending the actor to more exchanges, I noticed something I hadn't planned for: Bybit — which had worked fine from day one — started returning HTTP 403 from the exact same server. Same code, same IP range, different day. No announcement, no changelog entry I could find. It just started blocking.

That's the actual lesson here: an API block you hit once isn't necessarily permanent, and a block you've never hit isn't necessarily safe either. Both directions can change without warning.

The fix: proxy only what actually needs it

The obvious fix is "route everything through a residential proxy." The problem: residential proxy bandwidth costs real money, and 5 of my 7 exchanges don't need it at all. Paying proxy rates for every single request when only two exchanges are blocking would be wasteful — and slower, since residential circuits add latency direct connections don't have.

So the actual fix is per-exchange:

  • Detect the blocked ones by status code (451 for a hard geo-block, 403 for the softer anti-bot flavor)
  • Only those two calls get an explicit proxy URL
  • Everything else keeps hitting the exchange directly, fast and free
async def _get_json(client, url, params=None, *, proxy_url=None):
    if proxy_url:
        async with httpx.AsyncClient(proxy=proxy_url) as proxied:
            r = await proxied.get(url, params=params, ...)
    else:
        r = await client.get(url, params=params, ...)
    ...
Enter fullscreen mode Exit fullscreen mode

One conditional client, one proxy URL passed down only where it's needed. The other 5 exchanges never touch the proxy at all.

Worth re-testing what you gave up on

The bigger habit change: I'd marked Binance as "permanently excluded" in my own notes and never looked back. That was fine for Binance (still 451, still needs the proxy). But I would have shipped Bybit broken silently if I hadn't happened to be touching that code again for an unrelated reason.

If you maintain anything that scrapes or calls third-party APIs long enough, the honest move is to periodically re-run the sources you've already integrated, not just the ones you're adding — blocks appear on a schedule you don't control.


I'm 0xGollum, I build small data actors on Apify — mostly scrapers and monitors for signals I found annoying to track by hand. If you're curious what else I'm shipping: linktr.ee/0xGollum



Enter fullscreen mode Exit fullscreen mode

Top comments (0)