Crypto API rate limits: 429 is a pause, not a dead end
When an AI trading agent gets HTTP 429 from a crypto API, do not mint a new key and do not retry in a tight loop. Read Retry-After, sleep that many seconds, then retry the same request. That is the whole per-minute answer.
The second question is whether backoff is enough. A 429 means you hit the burst or daily quota on this key. A 403 means the endpoint is plan-gated. Different errors, different next steps: wait, or upgrade the same X-API-Key. This is the agent path, not generic exponential backoff.
Limits are per key (scope: "api_key"), not per account:
| Tier | Burst | Daily | What it is for |
|---|---|---|---|
| Free | 10 req/min | 1,000/day (100 until the email is confirmed) | Live market-wide data, no card |
| Pro | 30 req/min | 10,000/day | Full-universe quant, gamma, whales, books, indicators |
| Pro Plus | 120 req/min | 50,000/day | Historical / bulk / Parquet backtesting |
A 429 on /api/* is JSON, never HTML, and carries fields so you never parse the sentence:
{
"detail": {
"error": "rate_limit_exceeded",
"message": "Per-minute rate limit of 10 requests exceeded on this Free API key.",
"limit": 10,
"window": "1m",
"tier": "free",
"scope": "api_key",
"retry_after": 43
}
}window tells you whether you burned the minute or the day — "1m" versus a daily-quota 429. The message names the tier, so a paid key can never be misread as a free-tier limit.
If the key is still unverified Free, a daily 429 also carries upgrade_available, upgrade_message and verified_daily_limit. Confirming the email lifts that same key from 100 to 1,000 requests/day, turns Pro on for 24 hours, and emails a discount code. An agent cannot click the link for you, but POST /api/v1/auth/resend-verify re-sends it and GET /api/v1/auth/keys/me reports email_verified.
Read Retry-After, then sleep
Every 429 sends a Retry-After header (integer seconds) and the full X-RateLimit-* set. Prefer the header over guessing, and fall back to detail.retry_after if a proxy stripped it.
import time
import requests
BASE = "https://cryptodataapi.com/api/v1"
HEADERS = {
"X-API-Key": "cdk_live_yourkey",
"User-Agent": "cryptodataapi-python/1.0",
}
def get(path, *, max_retries=3, **params):
url = f"{BASE}{path}"
for attempt in range(max_retries + 1):
r = requests.get(url, headers=HEADERS, params=params, timeout=30)
if r.status_code != 429:
return r
wait = r.headers.get("Retry-After")
if wait is None:
body = r.json().get("detail") or {}
wait = body.get("retry_after", 60)
time.sleep(int(wait))
return r # last 429 - caller decides whether to upgradeDo not invent your own exponential backoff first. Jitter is fine when Retry-After is missing; when it is present, honour it.
- A per-minute 429 recovers in under a minute. Sleep and continue.
- A daily-quota 429 does not recover until UTC midnight (
X-RateLimit-Reset-Day). That is the conversion signal, not a retry signal. - Prefer
GET /api/v1/dailyover fanning out ten endpoints. Responses carryCache-Control, so polling faster than the refresh cadence just spends quota.
HTTP 429 vs 403: rate limit vs plan-gated
Treat these as different branches. Mixing them up is how agents mint a second key, or retry a call that will never succeed on Free.
429 — rate limit. The key is allowed on this path; you went over 10/min or over the daily cap. Back off. Optionally upgrade if remaining-day is already near zero and the agent still has work.
403 — plan-gated. The key is valid and not over quota. This endpoint or coin scope needs a higher tier:
{
"detail": {
"error": "pro_required",
"message": "This data requires a Pro or Pro Plus tier API key. See https://cryptodataapi.com/pricing",
"required_tier": "pro",
"pricing_url": "https://cryptodataapi.com/pricing"
}
}Branch on required_tier ("pro" or "pro_plus") and pricing_url. Sleeping on a 403 does nothing — the next call is still 403.
401 — no key or invalid key. Different again. Unauthenticated 401s carry mint_url (POST /api/v1/auth/keys) and WWW-Authenticate: ApiKey … header="X-API-Key". Mint once, then keep that key.
def classify(response):
code = response.status_code
detail = (response.json() or {}).get("detail") or {}
if code == 429:
return "rate_limit", detail
if code == 403:
return "plan_gated", detail # required_tier, pricing_url
if code == 401:
return "auth", detail # detail.get("mint_url")
return "ok", detailA 403 on /api/v1/quant/market or /api/v1/quant/gex means Pro is required — not that you are being throttled.
Watch remaining-day headers before you hit the wall
Successful authenticated responses carry the same header set a 429 does. Read them on the 200s, so the 429 never arrives:
| Header | Meaning |
|---|---|
X-RateLimit-Limit-Minute | Burst cap for this key (Free 10, Pro 30, Pro Plus 120) |
X-RateLimit-Remaining-Minute | Calls left in the current minute |
X-RateLimit-Reset-Minute | Unix timestamp when the minute window resets |
X-RateLimit-Limit-Day | Daily cap (unverified Free 100, verified Free 1,000, Pro 10,000) |
X-RateLimit-Remaining-Day | Calls left today |
X-RateLimit-Reset-Day | Unix timestamp when the daily window resets (UTC midnight) |
Header names are case-insensitive; most clients see x-ratelimit-remaining-day.
def remaining(response):
h = {k.lower(): v for k, v in response.headers.items()}
def n(name):
v = h.get(name)
return int(v) if v is not None else None
return {
"remaining_minute": n("x-ratelimit-remaining-minute"),
"remaining_day": n("x-ratelimit-remaining-day"),
"limit_minute": n("x-ratelimit-limit-minute"),
"limit_day": n("x-ratelimit-limit-day"),
"reset_minute": n("x-ratelimit-reset-minute"),
"reset_day": n("x-ratelimit-reset-day"),
}If remaining_minute is 1 and you are about to fan out, batch or wait. If remaining_day is in the low tens and the agent still has a session's worth of work, that is the free-vs-Pro decision — not another GET.
You can also ask the key about itself with GET /api/v1/auth/keys/me. daily_limit and per_minute_limit are the effective caps, not the tier headline: an unverified Free key reports 100/day even though the published Free tier is 1,000.
One caveat: not every 403 echoes X-RateLimit-*. Trust remaining-day on 200s and 429s, and do not require those headers on a 403 — required_tier is enough to decide.
Free vs Pro: when backoff is the wrong move
Backoff is right when:
windowis"1m"andX-RateLimit-Remaining-Dayis still healthy. You bursted. SleepRetry-After, continue.- You are prototyping and 1,000 verified Free calls/day covers the loop. Confirm the email first — a 10× daily lift on the same key, with no payment.
Upgrading is right when:
- You are hitting the daily cap, not just the minute. Free is 1,000/day; Pro is 10,000/day.
- The agent loop needs more than 10 calls in a single minute of tool use. Pro is 30 req/min.
- You are getting 403
pro_requiredon endpoints the agent actually needs — quant market/coins/risk, GEX, whales, full-universe books and liquidations, trading indicators. Sleeping will never unlock those.
Free is the live market-wide product at 10/min and 1,000/day. Pro is the same key with 3× the burst, 10× the daily allowance, and the per-coin quant suite, — current price on the pricing page. Do not mint a second Free key to dodge the cap — limits are per key by design, and it leaves you managing two identities for one agent.
Upgrade the same key with x402 agent-subscribe
This is the agent self-subscribe path: one endpoint, gasless USDC over x402, no dashboard, no human in the loop. Pass the existing X-API-Key. Key resolution is explicit:
- Include
X-API-Key→ renew or upgrade that key's subscription. - No key header → look up an existing key by wallet, or create a new one (returned once — save it).
Omit the header and you can accidentally mint. Send it and you cannot.
- Discover.
POSTwith nox-paymentheader returns HTTP 402 withaccepts[](network, asset, amount) and aPAYMENT-REQUIREDheader. - Sign. Off-chain EIP-712 USDC payment, zero gas. Base (
eip155:8453, recommended), Ethereum or Solana. Plans:monthly/annual= Pro,monthly_plus/annual_plus= Pro Plus. The 402 carries the exact amount in atomic USDC units (6 decimals), so read it from the response rather than hard-coding a figure. Live amounts:GET /api/v1/payments/plans. - Settle. Re-POST with the signed
x-paymentheader and your existing key. The key you already hold is now Pro.
# Discover the 402 (price + networks). Send the key you already have.
curl -X POST https://cryptodataapi.com/api/v1/payments/agent-subscribe \
-H "Content-Type: application/json" \
-H "X-API-Key: cdk_live_yourkey" \
-d '{"plan":"monthly"}'
# Then re-POST with the signed x-payment header to settle.
# monthly = Pro (30 req/min, 10,000/day). monthly_plus = Pro Plus.SUBSCRIBE = f"{BASE}/payments/agent-subscribe"
def upgrade_same_key(plan="monthly"):
r = requests.post(
SUBSCRIBE,
headers={**HEADERS, "Content-Type": "application/json"},
json={"plan": plan},
timeout=30,
)
if r.status_code == 402:
# Sign r.json()["accepts"] off-chain, then:
# headers["x-payment"] = signed_payload
# r = requests.post(SUBSCRIBE, headers=headers, json={"plan": plan})
return r
r.raise_for_status()
return rCard checkout (POST /api/v1/payments/stripe/checkout) is a browser redirect — not for an unattended agent. After settlement, GET /api/v1/auth/keys/me reports Pro limits on the same key. No MCP re-add, no re-install.
A practical agent loop
Six rules, in order of when they fire:
- Authenticate once.
X-API-Keyon every call, and send a namedUser-Agent— Python's defaulturllibstring is blocked at the edge. - Prefer one fat call.
GET /api/v1/dailyfirst; append?format=markdownfor an LLM. - Read remaining-day on every 200. If remaining-minute is 0, wait for reset. If remaining-day is low, upgrade rather than discovering it via a 429.
- On 429: sleep
Retry-After. Ifwindowis daily, or you have retried the minute twice, upgrade. Ifemail_verifiedis false and the cap is 100, resend verify — cheaper than Pro. - On 403: do not retry. If
required_tieris"pro", callagent-subscribewith{"plan":"monthly"}and the same key. - On 401: mint once at
POST /api/v1/auth/keys. Never mint inside a 429 handler.
An AI trading agent that hits a rate limit either waits, confirms its email, or pays. It does not open a new key.
When to upgrade to Pro
If the agent is already doing useful work on Free, the next step is not another market-data tutorial. It is headroom: 30 requests a minute and 10,000 a day, on the key you already have, plus the Pro endpoints a live agent actually calls.
- Agents:
POST /api/v1/payments/agent-subscribewith{"plan":"monthly"}and your existingX-API-Key. - Humans: the pricing page — card or USDC, monthly or annual.
- Discount:
SOCIAL50takes 50% off the first 3 months of Pro.
Caps, 429 behaviour and Retry-After are documented in the API docs. Any change to the error shape lands on the changelog, and every /api/* response carries an X-API-Version header so you can detect a shape change without diffing payloads.
Related reading: the candles + funding + OI spine for a trading agent, and minting a free key with no signup form.



