Why your bot gets 403 from Cloudflare (and how to harden a ccxt client)
Si automatizas un exchange con ccxt , tarde o temprano lo verás en los logs: rachas cortas de 403 Forbidden que pegan a fetch_balance , a los OHLCV o al saldo de earn, y que desaparecen solas a los pocos minutos. No es que tu API key esté mal. Es el WAF (Cloudflare) que muchos exchanges ponen delante de su REST, challengueando a algo que "parece un bot". Y tu bot es un bot — pero uno legítimo ,…
If you automate an exchange with ccxt, sooner or later you'll see it in the logs: short bursts of 403 Forbidden hitting fetch_balance, OHLCV, or earn balance, which disappear on their own after a few minutes. It's not that your API key is bad. It's the WAF (Cloudflare) that many exchanges put in front of their REST, challenging something that "looks like a bot".
And your bot is a bot — but a legitimate one, operating your own account against the official API. The problem isn't with permissions, it's with HTTP client reputation. This is about reducing WAF false positives, not evading any access control.
Two layers mitigate this:
I took this pattern from my own bot on OKX, after several bursts of 403, and published it as a library: ccxt-resilience (Apache-2.0).
1. Make the WAF challenge less: harden
A default ccxt client announces itself as it is. Adjusting a browser User-Agent, the Accept-Language header, and a loose timeout makes Cloudflare challenge it less frequently:
```python
import ccxt
from ccxt_resilience import harden
exchange = harden(ccxt.okx({
"apiKey": ...,
"secret": ...,
"password": ...,
}))
```
harden touches an already built client, returns the same object (chainable), and never breaks its construction: if something fails when setting attributes, it leaves them as they were.
2. Retry only what's due: with_retry
The temptation is to wrap everything in a try/except that retries. This is a trap: retrying a credentials or funds error only wastes time, ends up just as bad, and hides logic bugs behind waits. The key is to retry only transient ones — 403/Cloudflare, 429, timeouts — with exponential backoff and jitter, and re-throw real errors right away:
```python
from ccxt_resilience import with_retry
balance = with_retry(exchange.fetch_balance)
ohlcv = with_retry(exchange.fetch_ohlcv, "BTC/USDT", timeframe="1m", attempts=4, base=1.0, max_s=8.0)
```
An authentication error is re-thrown immediately, without retrying. And if attempts are exhausted, the last exception is re-thrown, so your fail-safe handler keeps governing (e.g., returning the last cached value).
The i-th attempt wait is min(max_s, base * 2**i) + rand()*base: it grows bounded and jitter avoids multiple clients from retrying in sync.
Classification is explicit
What makes an exception retryable or not isn't hidden: it's a function you can inspect and replace.
```python
from ccxt_resilience import is_transient_error
# A Cloudflare 403? Yes. An invalid key? No.
is_transient_error(Exception("403 Forbidden Cloudflare")) # True
is_transient_error(Exception("invalid api key")) # False
```
With ccxt installed, it also classifies by exception type (DDoSProtection, RequestTimeout…); without it, by message. That's why ccxt is a soft dependency: the library works with or without it. And if your case is another API, with_retry accepts your own retry predicate.
Installation
pip install ccxt-resilience
The code, tests, and details are in the repo: github.com/isazajuancarlos/ccxt-resilience. It's small, stateless infrastructure; use it part by part.
Translated by urgent.news. Machine-written — may contain errors; check the original before relying on it.
