Date: 2026-03-14 Status: Draft Approach: Full Cloudflare Native (Approach A)
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.
┌─────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────┘
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;
}
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;
}
Crypto.com Exchange API v1:
https://api.crypto.com/exchange/v1BTC_USDT (underscore-separated)wss://stream.crypto.com/exchange/v1/market for market dataCoinbase Advanced Trade API:
https://api.coinbase.com/api/v3/brokerageBTC-USDT (hyphen-separated)wss://advanced-trade-ws.coinbase.com for market dataEvery trade passes through 10 mandatory checks before execution. The types of checks are hardcoded; only thresholds are configurable. All timestamps use UTC.
min($500, portfolio * 2%)? → REJECT| 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 |
// 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
}
}
Runs on Cloudflare Cron Triggers, executing every 1 minute.
{ action: BUY|SELL|HOLD, confidence: 0-1, pair: string }.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:
partially_filled, filledQty updated)Users can submit trades directly from the dashboard. Manual trades bypass the ML signal and confidence gate but still pass through all guardrails.
Six singleton Durable Objects hold all persistent state. All DO state uses canonical pair format (“BTC-USDT”).
{
// 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
}
{
openOrders: Order[];
history: Order[]; // Rolling 30-day window, older archived to KV (TRADE_HISTORY_KV)
}
(See Guardrails section above for full state definition.)
{
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.
{
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;
}
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.
| 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) |
/api/auth/login with { key: DASHBOARD_API_KEY }. Returns a JWT (HS256, signed with a Worker secret, 24h expiry). All subsequent requests include Authorization: Bearer <jwt>.DASHBOARD_API_KEY as a Bearer token directly (no JWT dance needed since it is machine-to-machine).Access-Control-Allow-Origin to the Cloudflare Pages dashboard URL (configured via DASHBOARD_ORIGIN environment variable). Credentials are allowed. Preflight requests are handled.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 |
Thin FastAPI wrapper deployed on Fly.io around the existing Python trading engine.
All requests require Authorization: Bearer <ML_SERVICE_KEY> header.
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 }
Existing pipeline: XGBoost + LSTM + Logistic regression ensemble. Each model votes, confidence is the weighted average agreement.
| 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 |
All secrets stored as Cloudflare Worker secrets, deployed via GitHub Actions from GitHub repository 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 |
| Binding | Purpose |
|---|---|
TRADE_HISTORY_KV |
Archived order history (older than 30 days) |
AUDIT_LOG_KV |
Archived audit events (older than 7 days) |
| 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 |
The agent supports two modes configured via the TRADING_MODE environment variable:
live — Real-money trading against production exchange APIspaper — Paper trading mode: all exchange calls are real (real prices, real orderbooks), but orders are simulated locally. P&L is tracked but no real money moves. Use this for validation before going live.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.
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/
Promise.allSettled — if one exchange is unreachable, trades route to the healthy exchange automatically. No manual intervention needed.clientOrderId (UUID). Both Crypto.com and Coinbase support idempotent order creation — retrying with the same clientOrderId returns the existing order instead of creating a duplicate.AuditEvent is logged with type balance_sync./predict calls. On timeout, the signal is treated as HOLD.GET /api/status for external monitoring (Uptime Robot, Cloudflare Health Checks). Alerts on: kill switch activation, daily loss limit hit, exchange API errors, ML service downtime. Alert channel: AuditLogDO events are available via the /api/events endpoint and MCP get_events tool.