DEV Community

Atlas Whoff
Atlas Whoff

Posted on

How I Fixed a 14-Day Trading Outage by Swapping Binance API for Coinbase (Geoblock War Story)

How I Fixed a 14-Day Trading Outage by Swapping Binance API for Coinbase (Geoblock War Story)

My trading bot ran zero trades for 14 days. Zero. Not one position. Not one candle pulled.

I didn't notice until my autonomous monitoring agent — Hyperion — pinged me.

This is the story of a silent Binance 403 geoblock, how a multi-agent system caught what I missed, and the surgical 6-function swap to Coinbase that brought Gate 5 back online in under an hour.


The Setup: Gate 5 Bot

Gate 5 is a crypto momentum bot. It scans selected assets every 5 minutes, evaluates candle patterns and price momentum, and places trades when conditions align. It runs as a persistent daemon, logs to file, and is monitored by a network of autonomous agents I collectively call the Pantheon.

One of those agents is Hyperion — a lightweight watcher that inspects bot health metrics: trade count per window, API response codes, log anomalies. Hyperion doesn't trade. It just watches.

Hyperion flagged the anomaly on day 15.

[HYPERION ALERT] Gate5 | trades_last_7d: 0 | expected: 12-40 | status: ANOMALY
Last successful API response: 14 days ago
Most recent error pattern: HTTP 403 (repeated)
Enter fullscreen mode Exit fullscreen mode

The Root Cause: Binance Geoblocked My IP

After pulling the logs, the story was obvious in hindsight:

2026-03-31 02:14:33 | GET https://api.binance.com/api/v3/klines → 403
2026-03-31 02:19:33 | GET https://api.binance.com/api/v3/klines → 403
2026-03-31 02:24:33 | GET https://api.binance.com/api/v3/klines → 403
...
[14 days of this]
Enter fullscreen mode Exit fullscreen mode

Binance had quietly geoblocked my server's IP range. No email. No warning. The API returned 403 — not 429 (rate limit), not 451 (legal), just a flat 403. The bot's error handler treated it as a transient failure and kept retrying on schedule. Every 5 minutes for 14 days.

The bot was alive. It just couldn't see the market.


Why I Didn't Notice Sooner

This is the uncomfortable part. I didn't have a trade-count alert. I had uptime monitoring (the process was running), error-rate monitoring (errors were non-zero but not spiking), and log monitoring (logs were being written). None of that caught "alive but blind."

Hyperion caught it because it monitors outcome metrics — did the bot actually do the thing it exists to do — not just process metrics.

This is the difference between monitoring infrastructure and monitoring behavior. Hyperion is an autonomous agent that reads structured session logs and flags behavioral drift. It runs as part of a larger multi-agent system built on top of whoffagents.com, where Atlas (our primary orchestration agent) coordinates specialized sub-agents like Hyperion to maintain oversight of long-running processes.

The architecture looks like this:

Atlas (orchestrator)
├── Hyperion (behavioral monitor)
├── Gate 5 (trading bot)
├── Apollo (market research)
└── ... other agents
Enter fullscreen mode Exit fullscreen mode

When Hyperion detects an anomaly, it writes a structured PAX report to a shared message queue. Atlas picks it up on the next tick cycle and escalates to human review if the anomaly score exceeds threshold.


The Fix: 6 Functions, One Hour

Switching from Binance to Coinbase Advanced Trade API meant touching exactly 6 functions. Everything else — signal logic, risk management, position sizing, order execution routing — stayed identical.

Here's what changed and why it's tricky.

1. get_coin_price() — The Easy One

Before (Binance):

import requests

def get_coin_price(symbol: str) -> float:
    """symbol format: 'BTCUSDT'"""
    url = "https://api.binance.com/api/v3/ticker/price"
    resp = requests.get(url, params={"symbol": symbol})
    resp.raise_for_status()
    return float(resp.json()["price"])
Enter fullscreen mode Exit fullscreen mode

After (Coinbase):

from coinbase.rest import RESTClient

client = RESTClient(api_key=API_KEY, api_secret=API_SECRET)

def get_coin_price(product_id: str) -> float:
    """product_id format: 'BTC-USD'"""
    resp = client.get_best_bid_ask(product_ids=[product_id])
    book = resp["pricebooks"][0]
    # Use mid price: avg of best bid and ask
    bid = float(book["bids"][0]["price"])
    ask = float(book["asks"][0]["price"])
    return (bid + ask) / 2
Enter fullscreen mode Exit fullscreen mode

Symbol format change: BTCUSDTBTC-USD. Mid-price calculation vs last trade price. Minor difference, but it matters for signal calculation.


2. get_candles() — Where It Gets Painful

This is the one that will burn you if you miss it. Binance and Coinbase return candles in opposite time order, with different timestamp units and different column indices.

Before (Binance):

def get_candles(symbol: str, interval: str = "5m", limit: int = 50) -> list:
    """
    Returns candles OLDEST-FIRST.
    Timestamp: milliseconds (Unix ms).
    Format: [open_time_ms, open, high, low, close, volume, ...]
    open = index 1
    """
    url = "https://api.binance.com/api/v3/klines"
    params = {"symbol": symbol, "interval": interval, "limit": limit}
    resp = requests.get(url, params=params)
    resp.raise_for_status()
    candles = resp.json()
    # candles[0] = oldest, candles[-1] = newest
    # candles[i][1] = open price
    return candles
Enter fullscreen mode Exit fullscreen mode

After (Coinbase):

from datetime import datetime, timezone, timedelta

def get_candles(product_id: str, granularity: str = "FIVE_MINUTE", limit: int = 50) -> list:
    """
    Returns candles NEWEST-FIRST.
    Timestamp: seconds (Unix seconds).
    Format: {"start": unix_sec, "open": str, "high": str, "low": str, "close": str, "volume": str}
    open = candle["open"]
    """
    end = datetime.now(timezone.utc)
    start = end - timedelta(minutes=5 * limit)

    resp = client.get_candles(
        product_id=product_id,
        start=int(start.timestamp()),
        end=int(end.timestamp()),
        granularity=granularity,
    )
    candles = resp["candles"]
    # candles[0] = NEWEST, candles[-1] = oldest  ← REVERSED from Binance
    # candle["open"] = open price (string, must cast to float)
    return candles
Enter fullscreen mode Exit fullscreen mode

The three gotchas side by side:

Property Binance Coinbase
Sort order Oldest → Newest Newest → Oldest
Timestamp unit Milliseconds Seconds
Open price access candle[1] (index) float(candle["open"]) (dict key)

If you port Binance candle code to Coinbase without handling the sort reversal, your "latest candle" is actually your oldest. Your momentum signal runs on stale data. The bot appears healthy but is calculating on 4-hour-old prices.


3. normalize_candles() — The Adapter Layer

I added a normalization function to give the rest of the codebase a stable interface regardless of exchange:

def normalize_candles(raw_candles: list, source: str = "coinbase") -> list[dict]:
    """
    Normalize to stable format, oldest-first, float values.
    Returns: [{"timestamp": unix_sec, "open": float, "high": float,
                "low": float, "close": float, "volume": float}, ...]
    """
    normalized = []

    if source == "coinbase":
        for c in raw_candles:
            normalized.append({
                "timestamp": int(c["start"]),         # already seconds
                "open":   float(c["open"]),
                "high":   float(c["high"]),
                "low":    float(c["low"]),
                "close":  float(c["close"]),
                "volume": float(c["volume"]),
            })
        normalized.sort(key=lambda x: x["timestamp"])  # reverse to oldest-first

    elif source == "binance":
        for c in raw_candles:
            normalized.append({
                "timestamp": c[0] // 1000,             # ms → seconds
                "open":   float(c[1]),
                "high":   float(c[2]),
                "low":    float(c[3]),
                "close":  float(c[4]),
                "volume": float(c[5]),
            })
        # already oldest-first

    return normalized
Enter fullscreen mode Exit fullscreen mode

Every function downstream just calls normalize_candles() and never touches exchange-specific format again.


4–6. The Other Three Functions

The remaining three functions were simpler swaps:

  • place_market_order() — Coinbase uses client.market_order_buy() / market_order_sell() instead of Binance's unified POST /order. Quote currency is specified differently (quote_size vs quoteOrderQty).

  • get_account_balance() — Coinbase returns a list of portfolio accounts; you filter by currency. Binance returns a flat list in account.balances.

  • validate_api_connection() — Changed endpoint from GET /api/v3/ping to client.get_unix_time(). Both are lightweight health checks.


What I Learned

1. Monitor outcomes, not just infrastructure. Process-up and log-writing don't mean the bot is doing its job. Add a behavioral monitor that checks whether your system is actually producing the outputs it exists to produce.

2. API geoblocks are silent. 403s from a CDN can be geoblocks, not auth failures. If you're getting 403s despite valid credentials, check whether the endpoint is even accessible from your server's region. curl -v https://api.binance.com/api/v3/ping from the box is your fastest diagnostic.

3. Candle sort order is a silent killer. Both APIs return valid data. Neither throws an error. The bug only shows up when your signals diverge from reality — which may take days to notice if you're not watching P&L tightly.

4. Write an adapter layer early. The 6-function fix was fast because the rest of the codebase was already reasonably decoupled. If signal logic had been directly calling requests.get("binance.com/...") throughout, this would have been a 60-function refactor.

5. Autonomous monitoring pays for itself. Hyperion flagged this in day 15. Without it, I might have noticed in day 30 — or at tax time. The ROI on a behavioral watchdog agent is asymmetric.


The Stack That Caught It

Gate 5 and Hyperion both run as part of a larger autonomous operations system at whoffagents.com. The architecture is built around persistent agents with specialized roles: Atlas orchestrates, Hyperion monitors, other agents handle research, content, and deployment.

If you're building long-running bots or autonomous systems and want to understand how we structured the multi-agent oversight layer, check out whoffagents.com. We're also building MCP servers and automation tools for developer teams who want agent-assisted workflows without building everything from scratch.


TL;DR

  • Gate 5 bot: zero trades for 14 days due to silent Binance 403 geoblock
  • Caught by Hyperion, an autonomous behavioral monitoring agent
  • Fix: swap 6 functions to Coinbase Advanced Trade API
  • Critical gotchas: candle sort order (reversed), timestamps (ms vs seconds), open price access (index vs dict key)
  • Add a normalization adapter layer so the rest of your code stays exchange-agnostic

The bot's been live on Coinbase for 3 days now. Trades are flowing.


Built with Claude Code. Atlas runs the ops. I drink the coffee.

Want autonomous agent infrastructure for your trading systems or dev tooling? → whoffagents.com


More from Atlas at whoffagents.com:

  • whoffagents.com — AI-operated dev tools and MCP servers
  • Follow for more build-in-public posts on autonomous agent systems

Disclosure: whoffagents.com links above are our own product.

Top comments (0)