Connecting...
Last Update: --:--:--

Trading Agent Design Spec

Date: 2026-03-14 Status: Draft Approach: Full Cloudflare Native (Approach A)

Overview

A full-scale autonomous trading agent that operates across Crypto.com and Coinbase exchanges. The system uses Cloudflare Workers + Durable Objects for the agent runtime, a Python ML inference service on Fly.io for signal generation, and the existing React dashboard on Cloudflare Pages for monitoring and control.

Goals

Non-Goals

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    CLOUDFLARE PAGES                          │
│  React Dashboard (Vite + Tailwind)                          │
│  - Portfolio view (aggregated across exchanges)              │
│  - Live signals & trade history                              │
│  - Guardrail configuration                                   │
│  - Manual trade override / kill switch                       │
│  - Pair whitelist management                                 │
└─────────────────────────┬───────────────────────────────────┘
                          │ REST + WebSocket
┌─────────────────────────┼───────────────────────────────────┐
│               CLOUDFLARE WORKERS                             │
│                                                              │
│  Trading Agent API (TypeScript)                              │
│  - /api/portfolio    - /api/trades                           │
│  - /api/signals      - /api/guardrails                       │
│  - /api/pairs        - /api/kill-switch                      │
│  - /api/trade        - /api/market/:pair                     │
│  - WebSocket /api/ws                                         │
│                                                              │
│  Durable Objects:                                            │
│  - AgentStateDO    (positions, balances, P&L, status)        │
│  - OrderManagerDO  (open orders, fill history)               │
│  - GuardrailsDO    (risk config, runtime state)              │
│  - MarketDataDO    (price cache, orderbook, candles)         │
│  - TradingPairsDO  (whitelist, exchange intersection)        │
│                                                              │
│  Exchange Adapters:                                          │
│  - CryptoComAdapter  (REST + WebSocket)                      │
│  - CoinbaseAdvancedAdapter (REST + WebSocket)                │
│                                                              │
│  Scheduled Triggers (Cron):                                  │
│  - Every 1m: fetch prices, check signals, execute            │
│  - Every 5m: rebalance check, guardrail audit                │
│  - Every 1h: pair discovery, portfolio sync                  │
│  - Every 24h: P&L report, model retrain trigger              │
└─────────────────────────┬───────────────────────────────────┘
                          │ HTTP
┌─────────────────────────┼───────────────────────────────────┐
│          ML INFERENCE SERVICE (Fly.io)                       │
│  FastAPI wrapper around existing trading engine               │
│  - POST /predict  → BUY/SELL/HOLD + confidence               │
│  - POST /retrain  → trigger model retraining                 │
│  - GET  /health   → model status                             │
│  XGBoost + LSTM + Logistic ensemble                          │
└─────────────────────────────────────────────────────────────┘
                          │
┌─────────────────────────┼───────────────────────────────────┐
│          MCP SERVER (TypeScript)                              │
│  Tools: get_portfolio, get_signals, execute_trade,           │
│  set_guardrails, manage_pairs, kill_switch,                  │
│  get_trade_history, get_pnl_report, get_market_data          │
└─────────────────────────────────────────────────────────────┘

Exchange Adapter Interface

Both Crypto.com and Coinbase implement a unified TypeScript interface. The agent logic is exchange-agnostic.

interface Exchange {
  readonly name: 'crypto.com' | 'coinbase';

  // Market Data
  getTicker(pair: string): Promise<Ticker>;
  getOrderbook(pair: string, depth?: number): Promise<Orderbook>;
  getCandles(pair: string, interval: string, limit?: number): Promise<Candle[]>;
  getTrades(pair: string, limit?: number): Promise<Trade[]>;
  getAvailablePairs(): Promise<TradingPair[]>;

  // Account
  getBalances(): Promise<Balance[]>;
  getBalance(asset: string): Promise<Balance>;
  getFeeSchedule(): Promise<{ makerRate: number; takerRate: number }>;

  // Orders — clientOrderId is mandatory for idempotency
  placeLimitOrder(pair: string, side: 'buy'|'sell', price: number, qty: number, clientOrderId: string): Promise<Order>;
  placeMarketOrder(pair: string, side: 'buy'|'sell', qty: number, clientOrderId: string): Promise<Order>;
  cancelOrder(orderId: string): Promise<void>;
  getOrder(orderId: string): Promise<Order>;
  getOpenOrders(pair?: string): Promise<Order[]>;

  // WebSocket subscriptions
  subscribeToTicker(pair: string, callback: (ticker: Ticker) => void): void;
  subscribeToOrderUpdates(callback: (order: Order) => void): void;
  disconnect(): void;

  // Normalization
  normalizePair(pair: string): string;
}

Shared Types

interface Ticker {
  pair: string;       // Canonical format: "BTC-USDT"
  bid: number;
  ask: number;
  last: number;
  volume24h: number;
  timestamp: string;
}

interface Orderbook {
  pair: string;
  bids: [number, number][];  // [price, qty]
  asks: [number, number][];
  timestamp: string;
}

interface Candle {
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number;
  timestamp: string;
}

interface Trade {
  id: string;
  pair: string;
  side: 'buy' | 'sell';
  price: number;
  qty: number;
  timestamp: string;
}

interface TradingPair {
  symbol: string;       // Canonical: "BTC-USDT"
  base: string;         // "BTC"
  quote: string;        // "USDT"
  minQty: number;
  maxQty: number;
  qtyStep: number;
  priceStep: number;    // Minimum price increment (tick size)
  minNotional: number;  // Min order value in quote currency
}

interface Balance {
  asset: string;
  available: number;
  locked: number;       // In open orders
  total: number;
}

interface Order {
  id: string;
  clientOrderId: string;      // Client-generated UUID for idempotency
  pair: string;
  side: 'buy' | 'sell';
  type: 'limit' | 'market';
  status: 'open' | 'filled' | 'partially_filled' | 'cancelled';
  price: number | null;       // null for market orders
  filledPrice: number | null;
  qty: number;
  filledQty: number;
  fee: number | null;         // Fee paid on fill
  feeAsset: string | null;    // Asset fee was charged in
  exchange: 'crypto.com' | 'coinbase';
  createdAt: string;
  updatedAt: string;
}

interface Signal {
  action: 'BUY' | 'SELL' | 'HOLD';
  confidence: number;         // 0-1
  pair: string;               // Canonical format
  timestamp: string;
}

interface AuditEvent {
  id: string;
  type: 'trade_executed' | 'trade_rejected' | 'guardrail_triggered' | 'kill_switch' |
        'config_changed' | 'pair_changed' | 'balance_sync' | 'agent_status_change';
  actor: 'system' | 'user' | 'mcp';
  details: Record<string, unknown>;
  timestamp: string;
}

Exchange-Specific Details

Crypto.com Exchange API v1:

Coinbase Advanced Trade API:

Guardrails System

Every trade passes through 10 mandatory checks before execution. The types of checks are hardcoded; only thresholds are configurable. All timestamps use UTC.

Check Pipeline

  1. Kill switch — Is the agent halted? → REJECT
  2. Max trade size — Does this trade exceed min($500, portfolio * 2%)? → REJECT
  3. Daily loss limit — Has today’s realized P&L (UTC day) dropped > 5% from day-open? → REJECT + auto-halt
  4. Rolling drawdown — Has portfolio value dropped > 10% from its trailing 7-day peak? → REJECT + auto-halt
  5. Rate-of-loss — Has realized loss exceeded 2% in any rolling 30-minute window? → REJECT + 15-min pause
  6. Pair whitelist — Is this pair in the active whitelist? → REJECT
  7. Per-pair cooldown — Has 60s elapsed since last trade on this pair? → REJECT
  8. Global rate limit — No more than 5 trades per 5-minute window across all pairs → REJECT
  9. Portfolio concentration — Would this push any single asset > 25% of portfolio (excl. stablecoins: USDT, USDC, DAI, BUSD)? → REJECT
  10. Sufficient balance — Does the chosen exchange have enough funds? → REJECT

Default Configuration

Guardrail Default Scaling
Max single trade $500 or 2% of portfolio (whichever is smaller) Portfolio size
Daily loss limit 5% drawdown from UTC day-open balance Portfolio size
Rolling drawdown 10% from trailing 7-day peak Portfolio size
Rate-of-loss 2% in any 30-minute window (triggers 15-min pause) Portfolio size
Max concentration 25% in any single asset (excl. stablecoins) Portfolio size
Per-pair cooldown 60 seconds per pair Fixed
Global rate limit 5 trades per 5-minute window Fixed
Kill switch OFF Manual toggle
Approved pairs Top 10 by market cap Dashboard whitelist
Stablecoins USDT, USDC, DAI, BUSD Configurable list

Kill Switch Behavior

Durable Object State

// GuardrailsDO
{
  config: {
    maxTradeUSD: 500,
    maxTradePercent: 0.02,
    dailyLossLimit: 0.05,
    rollingDrawdownLimit: 0.10,
    rollingDrawdownWindowDays: 7,
    rateOfLossLimit: 0.02,
    rateOfLossWindowMinutes: 30,
    rateOfLossPauseMinutes: 15,
    maxConcentration: 0.25,
    cooldownSeconds: 60,
    globalRateLimitTrades: 5,
    globalRateLimitWindowMinutes: 5,
    stablecoins: ['USDT', 'USDC', 'DAI', 'BUSD'],
    killSwitch: false
  },
  state: {
    todayDate: string,               // UTC date "2026-03-14"
    todayRealizedPnL: number,
    trailing7DayPeak: number,        // Highest portfolio value in last 7 days
    recentPnLWindow: Array<{ pnl: number; timestamp: string }>,  // Rolling 30-min loss tracking
    lastTradePerPair: Record<string, string>,  // pair → ISO timestamp
    recentTradeTimestamps: string[],  // Global rate limit tracking (last 5 min)
    pausedUntil: string | null,       // Rate-of-loss pause expiry
    haltReason: string | null
  }
}

Trading Agent Decision Loop

Runs on Cloudflare Cron Triggers, executing every 1 minute.

Flow

  1. Fetch Market Data — Parallel requests to both exchanges for all active pairs. Cache in MarketDataDO.
  2. Request ML Signal — POST to Fly.io inference service with candles, orderbook snapshot, volume. Returns { action: BUY|SELL|HOLD, confidence: 0-1, pair: string }.
  3. Confidence Gate — If action is HOLD or confidence < 0.7 (configurable), skip. Dashboard shows “suggested” badge for 0.5-0.7 confidence signals.
  4. Smart Order Router — Compare effective price (price + fees) across both exchanges. Pick best execution venue. Check balance availability on chosen exchange.
  5. Guardrails Check — Run the 10-check pipeline. Any failure rejects the trade with a logged reason.
  6. Execute Order — Place via chosen exchange adapter. Record in OrderManagerDO. Update AgentStateDO.
  7. Post-Trade — Update P&L, check if guardrail thresholds changed, push real-time update to dashboard via WebSocket.

Smart Order Router

interface RouteResult {
  exchange: Exchange;
  effectivePrice: number;
  estimatedFee: number;
  hasLiquidity: boolean;
}

async function routeOrder(
  pair: string,
  side: 'buy' | 'sell',
  qty: number,
  orderType: 'limit' | 'market',
  adapters: { cryptoCom: Exchange; coinbase: Exchange }
): Promise<RouteResult> {
  // Use allSettled so one exchange being down doesn't block the other
  const results = await Promise.allSettled([
    getExchangeQuote(adapters.cryptoCom, pair, side, qty, orderType),
    getExchangeQuote(adapters.coinbase, pair, side, qty, orderType),
  ]);

  const quotes = results
    .filter((r): r is PromiseFulfilledResult<RouteResult> => r.status === 'fulfilled')
    .map(r => r.value)
    .filter(q => q.hasLiquidity);

  if (quotes.length === 0) throw new Error(`No exchange available for ${pair}`);
  if (quotes.length === 1) return quotes[0];

  // For buys: lowest effective price wins. For sells: highest wins.
  return side === 'buy'
    ? quotes.reduce((a, b) => a.effectivePrice <= b.effectivePrice ? a : b)
    : quotes.reduce((a, b) => a.effectivePrice >= b.effectivePrice ? a : b);
}

async function getExchangeQuote(
  exchange: Exchange,
  pair: string,
  side: 'buy' | 'sell',
  qty: number,
  orderType: 'limit' | 'market'
): Promise<RouteResult> {
  const [ticker, orderbook, fees] = await Promise.all([
    exchange.getTicker(pair),
    exchange.getOrderbook(pair, 20),
    exchange.getFeeSchedule(),
  ]);

  // Use correct fee rate based on order type
  const feeRate = orderType === 'limit' ? fees.makerRate : fees.takerRate;

  // Check orderbook depth for slippage estimation
  const levels = side === 'buy' ? orderbook.asks : orderbook.bids;
  const { avgPrice, filled } = estimateSlippage(levels, qty);

  const price = side === 'buy' ? ticker.ask : ticker.bid;
  const effectivePrice = avgPrice * (1 + (side === 'buy' ? feeRate : -feeRate));

  return { exchange, effectivePrice, estimatedFee: avgPrice * qty * feeRate, hasLiquidity: filled };
}

Partial fill handling: When an order fills partially, the agent:

  1. Records the partial fill in OrderManagerDO (status: partially_filled, filledQty updated)
  2. Updates AgentStateDO position with the filled quantity
  3. Leaves the remaining quantity as an open order (does not cancel automatically)
  4. The partial fill does NOT consume the cooldown — only a full fill or cancellation resets it
  5. P&L is calculated incrementally on the filled portion

Manual Override

Users can submit trades directly from the dashboard. Manual trades bypass the ML signal and confidence gate but still pass through all guardrails.

Durable Objects Data Model

Six singleton Durable Objects hold all persistent state. All DO state uses canonical pair format (“BTC-USDT”).

AgentStateDO

{
  // Positions keyed by pair, per-exchange tracking
  positions: Record<string, Record<'crypto.com' | 'coinbase', {
    qty: number;
    avgEntry: number;
  }>>;
  balances: Record<string, Record<string, number>>;  // exchange → asset → amount
  dailyPnL: { date: string; startBalance: number; current: number };  // UTC date
  totalPnL: { initialDeposit: number; allTimeUSD: number; percentReturn: number };
  lastSync: string;
  status: 'running' | 'halted';  // halted = kill switch, all orders cancelled
}

OrderManagerDO

{
  openOrders: Order[];
  history: Order[];  // Rolling 30-day window, older archived to KV (TRADE_HISTORY_KV)
}

GuardrailsDO

(See Guardrails section above for full state definition.)

MarketDataDO

{
  tickers: Record<string, Record<string, Ticker>>;  // pair → exchange → ticker
  candles: Record<string, Candle[]>;                 // "pair:interval" → max 200 candles per key
  lastUpdate: string;
}

Eviction: Candles capped at 200 per pair:interval key. Tickers older than 5 minutes are pruned on read. Data for pairs removed from whitelist is evicted on the next hourly pair discovery cycle.

TradingPairsDO

{
  whitelist: string[];                              // User-managed (canonical format)
  exchangePairs: Record<string, string[]>;          // exchange → raw pairs
  intersection: string[];                           // Available on both (canonical)
  activePairs: string[];                            // whitelist ∩ intersection
  pairMetadata: Record<string, Record<'crypto.com' | 'coinbase', TradingPair>>;
  lastDiscovery: string;
}

AuditLogDO

A dedicated Durable Object for immutable event logging. Every state mutation is recorded.

{
  events: AuditEvent[];    // Rolling 7-day window in DO
  // Older events archived to KV (AUDIT_LOG_KV) daily
}

Events logged: trade executed, trade rejected (with guardrail reason), kill switch toggled, config changed, pair whitelist changed, balance sync discrepancy detected, agent status change. Each event includes an actor field: system, user, or mcp.

Dashboard API

REST Endpoints

Method Path Description
POST /api/auth/login Authenticate, returns JWT
GET /api/status Agent status (running/halted) + uptime
GET /api/portfolio Aggregated positions, balances, P&L
GET /api/portfolio/:exchange Per-exchange view
GET /api/trades Trade history (paginated, filterable by exchange/pair/side/dateRange)
GET /api/signals Current ML signals for active pairs
GET /api/pairs Active pairs + whitelist
PUT /api/pairs Update whitelist
GET /api/guardrails Current config + runtime state
PUT /api/guardrails Update thresholds
POST /api/trade Manual trade submission
PUT /api/kill-switch Toggle kill switch { enabled: boolean }
GET /api/orders/open Open orders across both exchanges
DELETE /api/orders/:id Cancel a specific order
GET /api/market/:pair Live market data
GET /api/events Paginated audit event log
WS /api/ws Real-time updates (prices, fills, signals, status)

Authentication

MCP Server

TypeScript MCP server exposing the agent’s capabilities to Claude.

Tool Parameters Description
get_agent_status Agent status (running/halted), uptime
get_portfolio exchange? Aggregated or per-exchange portfolio
get_signals pair? Current ML signals with confidence
execute_trade pair, side, qty, type?, price?, exchange? Place a trade (through guardrails)
get_open_orders pair? List open orders
cancel_order orderId Cancel a specific order
get_trade_history limit?, pair?, exchange? Recent fills with P&L
get_guardrails Current config and state
set_guardrails config Update thresholds
manage_pairs action, pairs Add/remove from whitelist
kill_switch enable Emergency halt toggle
get_market_data pair, type? Live prices, orderbook, candles
get_pnl_report period Daily/weekly/monthly P&L breakdown
get_events limit?, type? Recent audit events

ML Inference Service

Thin FastAPI wrapper deployed on Fly.io around the existing Python trading engine.

Authentication

All requests require Authorization: Bearer <ML_SERVICE_KEY> header.

Endpoints

POST /predict
  Body: {
    pair: string,
    candles: Candle[],        // Last 100 candles, 1h interval
    orderbook: {              // Top 10 levels
      bids: [number, number][],
      asks: [number, number][]
    },
    volume24h: number
  }
  Response: { action: "BUY"|"SELL"|"HOLD", confidence: number, pair: string }
  Timeout: 5 seconds. On timeout or error, the Worker treats the signal as HOLD.

POST /retrain
  Body: { pair: string }
  Response: { status: "started", estimatedTime: string }

GET /retrain/status
  Response: { status: "idle"|"training"|"completed"|"failed", lastCompleted: string, currentPair: string | null }

GET /health
  Response: { status: "healthy", modelsLoaded: string[], lastRetrain: string }

Model Ensemble

Existing pipeline: XGBoost + LSTM + Logistic regression ensemble. Each model votes, confidence is the weighted average agreement.

Cron Schedule

Interval Task Details
1 min Main trading loop Fetch prices → ML signal → route → guardrails → execute
5 min Rebalance check Verify portfolio concentration, sync balances
1 hour Pair discovery Re-fetch available pairs from both exchanges, update intersection
24 hours Daily report Generate P&L report, trigger model retrain if needed

Secrets & Configuration

All secrets stored as Cloudflare Worker secrets, deployed via GitHub Actions from GitHub repository secrets.

Secrets

Secret Purpose
CRYPTO_COM_API_KEY Crypto.com Exchange API key
CRYPTO_COM_API_SECRET Crypto.com HMAC signing secret
COINBASE_API_KEY Coinbase Advanced Trade API key
COINBASE_API_SECRET Coinbase JWT signing secret
ML_SERVICE_URL Fly.io inference service URL
ML_SERVICE_KEY Auth key for ML service
DASHBOARD_API_KEY Dashboard + MCP auth key
JWT_SECRET HS256 secret for signing dashboard JWTs
DASHBOARD_ORIGIN Cloudflare Pages URL for CORS

KV Namespaces

Binding Purpose
TRADE_HISTORY_KV Archived order history (older than 30 days)
AUDIT_LOG_KV Archived audit events (older than 7 days)

Durable Object Bindings

Binding Class Description
AGENT_STATE AgentStateDO Portfolio, positions, P&L
ORDER_MANAGER OrderManagerDO Open orders, fill history
GUARDRAILS GuardrailsDO Risk config and runtime state
MARKET_DATA MarketDataDO Price cache, candles
TRADING_PAIRS TradingPairsDO Whitelist, pair metadata
AUDIT_LOG AuditLogDO Immutable event log

Environment Modes

The agent supports two modes configured via the TRADING_MODE environment variable:

Paper mode uses the same code paths but wraps exchange adapters in a PaperTradingAdapter that intercepts order placement and simulates fills based on current market prices.

Project Structure

trading-agent/
├── wrangler.toml                  # Cloudflare Workers config (DO bindings, KV, cron, secrets)
├── package.json
├── tsconfig.json
├── src/
│   ├── index.ts                   # Worker entry point + cron handler
│   ├── router.ts                  # API route definitions
│   ├── auth.ts                    # JWT creation, validation, CORS middleware
│   ├── exchanges/
│   │   ├── interface.ts           # Exchange interface + all shared types
│   │   ├── crypto-com.ts          # Crypto.com adapter
│   │   ├── coinbase.ts            # Coinbase adapter
│   │   ├── paper-trading.ts       # PaperTradingAdapter wrapper
│   │   └── router.ts              # Smart order router
│   ├── durable-objects/
│   │   ├── agent-state.ts         # AgentStateDO
│   │   ├── order-manager.ts       # OrderManagerDO
│   │   ├── guardrails.ts          # GuardrailsDO
│   │   ├── market-data.ts         # MarketDataDO
│   │   ├── trading-pairs.ts       # TradingPairsDO
│   │   └── audit-log.ts           # AuditLogDO
│   ├── agent/
│   │   ├── decision-loop.ts       # Main trading loop
│   │   ├── signal-client.ts       # ML inference service client (5s timeout)
│   │   └── scheduler.ts           # Cron trigger handlers
│   ├── api/
│   │   ├── portfolio.ts           # Portfolio endpoints
│   │   ├── trades.ts              # Trade endpoints
│   │   ├── orders.ts              # Open orders + cancel
│   │   ├── signals.ts             # Signal endpoints
│   │   ├── guardrails.ts          # Guardrail endpoints
│   │   ├── pairs.ts               # Pair management endpoints
│   │   ├── events.ts              # Audit event log endpoint
│   │   ├── status.ts              # Agent status + health check
│   │   └── websocket.ts           # WebSocket handler
│   └── mcp/
│       └── server.ts              # MCP server tool definitions
├── ml-service/
│   ├── Dockerfile
│   ├── fly.toml
│   ├── requirements.txt
│   ├── main.py                    # FastAPI app
│   ├── predict.py                 # Prediction endpoint logic
│   └── models/                    # Trained model artifacts
└── tests/
    ├── exchanges/
    ├── durable-objects/
    ├── agent/
    └── api/

Risk Considerations

Exchange Connectivity

Double Execution Prevention

Balance Desynchronization

ML Service

Data Integrity

Operational