Rate limits, quotas, and tiers in practice

Read the X-RateLimit headers, back off on 429 and 5xx, handle the ML upgrade nudge, and manage keys in the TradeWave console.

Three limits, three behaviors

Most clients break on rate limits not because backoff is hard, but because they treat three different budgets as one. A robust TradeWave client respects three independent limits, and each one fails differently. That difference is the whole point - get it wrong and you either hammer a 429 into a ban or silently drop data on a perfectly good 200:

Limit Scope What happens at the edge
Per-minute rate limit Requests per account per minute A real HTTP 429, with Retry-After. Back off and retry.
Per-day request quota Requests per account per day A real HTTP 429 once exhausted. Resets daily.
Daily ML allowance ML scorings per account per day Never an error. HTTP 200 with ml: null and an upgrade nudge.

The headline distinction: rate and quota limits are transport-level guards that say "slow down" or "come back tomorrow," while the ML allowance is a product limit that degrades gracefully instead of failing. Your detected seasonal pattern still arrives even when your ML budget is gone. Everything below is illustrative; live responses carry a disclaimer and are educational, not personalized advice.

We do not hardcode the exact per-minute, per-day, or ML-allowance numbers here, because they vary by tier and can change. Read them off the API pricing page and your console. Qualitatively: free accounts get a small daily ML allowance, while Pro and Business are unlimited.

The X-RateLimit headers

Every response carries rate-limit headers so you never have to guess where you stand. Read them on the way out and you can stay just under the line:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1717329600
Header Meaning
X-RateLimit-Limit Your ceiling for the current window
X-RateLimit-Remaining Requests left in this window
X-RateLimit-Reset Unix epoch second when the window resets

When you do hit a 429, the response adds a Retry-After header (in seconds). Honor it exactly - it is the server telling you precisely how long to wait, which beats any guess.

opp_limit: the per-call result cap

Separate from rate limits, the discovery endpoints cap how many results one call returns via the limit query parameter (often called opp_limit internally). On GET /scan and GET /opportunities, ask for what you will actually use. You can run this exact call with no signup using the public demo token tw_demo_explore:

curl "https://api.tradewave.ai/v1/scan?markets=0,3&window=next_month&rank_by=sharpe&limit=10" \
  -H "Authorization: Bearer tw_demo_explore"

Swap in your own tw_live_xxx key from the console when you are ready - the demo token is throttled and shares a small ML allowance, which makes it a handy way to watch the metering below kick in.

Pulling limit=10 strong setups beats pulling hundreds you will never read. Smaller responses are faster, and since each call counts once against your per-minute and per-day budgets regardless of limit, asking for the right page size is free efficiency. Rank with rank_by (edge, win_rate, sharpe, ml, or avg_return) so the best results land at the top of your capped page.

The ML upgrade nudge is a 200, not a 429

This is the trap that breaks naive clients. When a free-tier caller runs out of daily ML allowance, the gateway does not return 429 and does not raise. It returns a normal HTTP 200 with the detected seasonal pattern fully intact, ml: null, an ml_remaining_today counter, and a gentle upgrade message in tier_notes:

{
  "symbol": "AAPL",
  "bias": "bullish",
  "stats": { "historical_win_rate": 0.74, "sharpe_ratio": 1.8, "years": "17" },
  "ml": null,
  "ml_remaining_today": 0,
  "tier_notes": "Daily ML allowance reached - seasonal pattern shown. Upgrade for unlimited ML."
}

Treat "out of ML budget" exactly like "ML not eligible for this setup": fall back to the seasonal stats and keep going. The same ml is None branch also covers ML-ineligible markets and holds longer than about 90 days, where you instead see tier_notes: "ML score not available for this setup - the ML model covers shorter seasonal holds (up to about 90 days)."

Two things worth pinning down. First, keep the probabilities distinct: historical_win_rate is the share of past years the seasonal pattern finished profitable, while ml_win_prob is the model's forward prediction - never collapse them into one "win rate." Second, GET /daily-pick always returns a free ML block that never touches your allowance, so it is the cheapest way to keep an ML read flowing through your pipeline.

A robust Python client

Here is a small, production-shaped client that reads the rate headers, backs off with exponential delay plus jitter on 429 and 5xx, honors Retry-After, and treats the ML nudge as a normal branch:

import random
import time
import requests

BASE = "https://api.tradewave.ai/v1"
HEADERS = {"Authorization": "Bearer tw_live_xxx"}


def get(path, params=None, max_retries=5):
    """GET with header-aware throttling and exponential backoff."""
    for attempt in range(max_retries + 1):
        resp = requests.get(f"{BASE}{path}", headers=HEADERS,
                             params=params, timeout=30)

        # Proactively slow down before we hit the wall.
        remaining = resp.headers.get("X-RateLimit-Remaining")
        if remaining is not None and int(remaining) <= 1:
            reset = int(resp.headers.get("X-RateLimit-Reset", "0"))
            pause = max(0, reset - int(time.time()))
            if pause:
                time.sleep(min(pause, 60))

        # Retry on rate-limit and transient server errors.
        if resp.status_code == 429 or resp.status_code >= 500:
            if attempt == max_retries:
                resp.raise_for_status()
            retry_after = resp.headers.get("Retry-After")
            if retry_after is not None:
                delay = float(retry_after)            # server told us exactly
            else:
                delay = (2 ** attempt) + random.uniform(0, 1)  # backoff + jitter
            time.sleep(delay)
            continue

        resp.raise_for_status()
        return resp.json()

    raise RuntimeError("exhausted retries")


def render(card):
    hist = card["stats"]["historical_win_rate"]   # share of profitable years
    ml = card.get("ml")
    if ml is None:
        # Ineligible market, >90-day hold, OR out of ML allowance - all land here.
        note = card.get("tier_notes", "ML not available.")
        left = card.get("ml_remaining_today")
        tail = f" (ml_remaining_today={left})" if left is not None else ""
        print(f"{card['symbol']}: hist_win_rate={hist:.0%}; ML n/a - {note}{tail}")
    else:
        # ml_win_prob is the MODEL's prediction, distinct from hist_win_rate.
        print(f"{card['symbol']}: hist_win_rate={hist:.0%}  "
              f"ml_win_prob={ml['ml_win_prob']:.0%}")


data = get("/scan", {"markets": "0,3", "window": "next_month",
                     "rank_by": "sharpe", "limit": 10})
for card in data["opportunities"]:
    render(card)

The key idea: a 429 is recoverable (wait, then retry), a 5xx is transient (back off, then retry), and the ML nudge is not a failure at all (it arrives as a 200 you simply branch on). Never retry a 200 just because ml is null.

Key management: create, rotate, revoke

Your API keys live in the console. Treat them like passwords:

Keep keys server-side and out of source control, logs, and client bundles. Rate limits and quotas are tracked per account, not per key - every key on your account shares one bucket, so adding keys never multiplies your quota. A separate key per service does not isolate or increase your budget; it buys you attribution (which key made which call), easy rotation, and blast-radius control (revoke one leaked key without touching the others).

Where to go next

Building an AI agent? The same seasonal patterns - and the same per-account limits and graceful ML metering - apply over MCP at https://mcp.tradewave.ai (sign in with your TradeWave account from ChatGPT or Claude.ai, or bring your own key in the Authorization header from dev tools), with tools like find_best_opportunities and analyze_symbol. We show the edge and the timing; you place the trade at any broker. Every response is educational and carries a disclaimer, not personalized advice.