Skip to main content
Glama
dearvn

tradebox-mcp

by dearvn
README.md
# tradebox-mcp

**A flight recorder + circuit breakers for AI trading agents.**

Exchanges are starting to let AI agents trade — and they admit they cannot see
what the agents are *thinking*. **tradebox** is a local proxy that sits between
your LLM (Claude, Cursor, or any MCP host) and any broker MCP server. It
records every tool call **and the agent's reasoning**, and it blocks any order
that breaks your limits — *before* the order reaches the exchange.

- **Zero agent changes** — point your MCP host at tradebox instead of the
  broker server; the agent sees the exact same tools.
- **Local-first** — your API keys go straight into the broker child process.
  tradebox never parses, logs, or transmits them, and makes **zero network
  calls** of its own.
- **Deny ≠ crash** — a blocked order comes back as a plain-language tool
  result the agent can read and adapt to, not a protocol error that sends it
  into a retry loop.

---

## How it works

tradebox speaks MCP on both sides: it is a *server* to your host and a
*client* to each broker server it spawns.

```mermaid
flowchart LR
    subgraph HOST["Your machine"]
        A["MCP host<br/>(Claude Desktop / Cursor)"]
        subgraph TB["tradebox-mcp"]
            G["Guardrail engine<br/>(allow / deny)"]
            R["Recorder<br/>(JSONL blackbox)"]
        end
        C["CCXT MCP server<br/>(child process)"]
        L[("~/.tradebox/logs/<br/>YYYY-MM-DD.jsonl")]
    end
    X["Exchange<br/>(Binance, …)"]

    A -- "stdio (JSON-RPC / MCP)" --> G
    G -- "allowed calls only" --> C
    G -.-> R
    R -.-> L
    C -- "HTTPS (your API keys<br/>never leave this hop)" --> X
```

Every `tools/call` goes through the same pipeline:

```mermaid
sequenceDiagram
    participant Agent as Agent (LLM)
    participant TB as tradebox
    participant Broker as CCXT MCP
    participant Ex as Exchange

    Note over Agent,Ex: ✅ order within limits
    Agent->>TB: createOrder BTC/USDT, $150
    TB->>TB: classify → trade.place<br/>guardrails → ALLOW
    TB->>Broker: forward
    Broker->>Ex: place order
    Ex-->>Broker: filled
    Broker-->>TB: result
    TB->>TB: log call + result (JSONL)
    TB-->>Agent: result

    Note over Agent,Ex: ⛔ order over the limit
    Agent->>TB: createOrder DOGE/USDT, $520
    TB->>TB: classify → trade.place<br/>guardrails → DENY (allowed_symbols)
    TB->>TB: log the denial
    TB-->>Agent: "Order denied: DOGE/USDT is not<br/>in allowed_symbols (BTC/USDT, ETH/USDT)."
    Note over Agent: agent reads the reason<br/>and adjusts — no crash loop
```

The order **never reaches the broker process** when a rule denies it — the
deny happens one process before your API keys are even involved.

---

## Quick start

**1 — Create your config** (keys stay on your machine; `chmod 600` it):

```bash
mkdir -p ~/.tradebox
cp config.example.yaml ~/.tradebox/config.yaml
chmod 600 ~/.tradebox/config.yaml
```

```yaml
# ~/.tradebox/config.yaml (minimal)
downstreams:
  ccxt:
    command: npx
    args: ["-y", "@lazydino/ccxt-mcp", "--config", "~/.tradebox/ccxt-accounts.json"]
    # ccxt-accounts.json holds your exchange keys (see config.example.yaml).
    # Use a read + trade key. NEVER enable withdrawals on it.

guardrails:
  allowed_symbols: ["BTC/USDT", "ETH/USDT"]
  max_order_notional: 200        # $ per single order
  max_orders_per_hour: 6
  max_daily_loss: 100            # trips the circuit breaker (UTC day)
  dry_run: true                  # ON by default — flip to false to go live
```

**2 — Point your MCP host at tradebox** instead of the broker server
(`claude_desktop_config.json` or `.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "trading": {
      "command": "npx",
      "args": ["-y", "tradebox-mcp", "run", "--config", "~/.tradebox/config.yaml"]
    }
  }
}
```

**3 — (Optional but recommended)** add one line to your agent's system prompt
so the blackbox records reasoning, not just actions:

> *Before every trading decision, call the `log_reasoning` tool with a short
> explanation of what you are about to do and why.*

That's it. The agent sees `ccxt__createOrder`, `ccxt__fetchTicker`, … as
usual. Nothing on the agent side changes.

---

## Guardrails

| Rule | Config key | What it does |
|---|---|---|
| Symbol whitelist | `allowed_symbols` | Deny any order outside your list |
| Order size cap | `max_order_notional` | Deny a single order over the cap (market orders are valued with a ticker price ≤ 60 s old — otherwise denied with "fetch the ticker first") |
| Rate limit | `max_orders_per_hour` | Runaway-loop breaker — the most common real failure mode of buggy agents (sliding 1-hour window) |
| **Daily loss breaker** | `max_daily_loss` | The main circuit breaker — see the diagram below |
| Trading hours | `trading_hours` | Only allow orders inside a UTC time window |
| Transfers | *(built-in)* | **Denied by default.** A trading agent has no business withdrawing funds. Opening it requires an explicit `allow_transfers: true` |
| Unknown tools | `unknown_tools` | A tool that no map recognizes and that *looks* mutating is **denied**, not assumed to be read-only |
| Panic button | `tradebox stop` | Instantly deny all trade tools, even while the agent is mid-run |

### The circuit breaker's life cycle

```mermaid
stateDiagram-v2
    [*] --> Trading
    Trading --> Locked : realized daily PnL ≤ −max_daily_loss
    Trading --> Locked : operator runs "tradebox stop"
    Locked --> Trading : operator runs "tradebox resume"
    Locked --> Locked : every trade.* call → denied<br/>(reads still pass through)

    note right of Locked
        The lock survives restarts —
        state is a projection of the log,
        so a crash never resets the breaker.
    end note
```

### Dry-run: audit your agent before you give it money

`dry_run: true` (the default) blocks every trade at the proxy, logs it as if
it were real, and returns a **simulated** fill. tradebox keeps a fake order
book so the simulation stays coherent: cancelling or fetching a simulated
order id gets a consistent answer, and every simulated result is tagged
`"simulated": true`. Run your agent for a week in dry-run, read the report,
*then* flip the switch.

---

## The blackbox

Every call — allowed or denied — is appended to
`~/.tradebox/logs/YYYY-MM-DD.jsonl`, one JSON event per line:

```json
{"ts":"2026-08-25T12:00:00.123Z","event":"tool_call","server":"ccxt","tool":"createOrder","category":"trade.place","args":{"symbol":"BTC/USDT","side":"buy","type":"limit","amount":0.02,"price":58900},"decision":"allow","latency_ms":840,"result":{"order_id":"123","filled":0.02,"avg_price":58895}}
{"ts":"2026-08-25T12:05:01.000Z","event":"tool_call","server":"ccxt","tool":"createOrder","category":"trade.place","args":{"symbol":"DOGE/USDT","side":"buy","amount":50000},"decision":"deny","rule":"allowed_symbols","latency_ms":2}
{"ts":"2026-08-25T12:05:04.500Z","event":"reasoning","text":"DOGE blocked. Holding BTC, waiting for the 58K retest."}
{"ts":"2026-08-25T13:00:00.000Z","event":"guardrail_trip","rule":"max_daily_loss","value":-102.5,"limit":-100,"action":"trading_locked"}
```

Secrets never enter the log: downstream `env` blocks are never seen by the
recorder, and any field named like `key|secret|token|password` is redacted.

### Drift report — is your agent still the agent you tested?

```
$ tradebox report --window 7d

AGENT BEHAVIOR REPORT              2026-08-18 → 2026-08-25
──────────────────────────────────────────────────────────
                      baseline (7d)    last 24h        Δ
orders/day                  4.2            11        ×2.6  ⚠
avg order notional        $145           $410        ×2.8  ⚠
symbols traded        BTC 82% · ETH 18%  +SOL 37%          ⚠ new symbol
avg hold time             3.1 h          22 min      ÷8.5  ⚠
denied calls                 0             7    max_order_notional ×5
realized PnL              +$83           −$61
──────────────────────────────────────────────────────────
⚠ BEHAVIORAL DRIFT: the agent is behaving differently than
  it did 7 days ago. Model update? Prompt change? Check
  before it costs you.
```

Offline, reads local JSONL only, no network.

---

## CLI

```
tradebox run --config <path>    start the proxy (spawned by your MCP host)
tradebox report [--window 7d]   behavior + drift report from local logs
tradebox stop                   PANIC — deny all trading immediately
tradebox resume                 clear the panic / daily-loss lock
```

---

## Honest limitations (v0.1)

We would rather list these than have you find them with real money:

1. **stdio downstreams only** (CCXT MCP and equivalents). HTTP transport for
   Binance Agent OS / Robinhood MCP is the top roadmap item.
2. **The daily-loss breaker sees fills through the proxy.** Fills are parsed
   from order results *and* from the agent's own `fetch_my_trades` /
   `fetch_closed_orders` calls. An agent that never fetches its fills leaves
   the breaker blind.
3. **Position tracking is an estimate** built from orders passing through the
   proxy — no exchange balance reconciliation yet.
4. **Dry-run fills are simulated instantly.** Real balance/position reads pass
   through unchanged and will not reflect simulated trades (every simulated
   result carries `"simulated": true`).
5. **All day boundaries are UTC.** One proxy instance at a time.

---

## Contributing a rule

One file, one interface — PRs welcome:

```ts
export interface GuardrailRule {
  name: string;
  // return null to pass; return a string to deny (the reason is sent to the LLM)
  check(call: ClassifiedToolCall, state: SessionState, cfg: Config): string | null;
}
```

Drop it in `src/guardrails/rules/`, register it in `engine.ts`, add a test.
See [docs/DESIGN.md](docs/DESIGN.md) for the architecture decisions and their
reasons.

## Roadmap

1. **HTTP transport proxy** → Binance Agent OS, Robinhood MCP
2. Position limits reconciled against exchange balances
3. Hosted dashboard + real-time alerts — *the local proxy and report stay
   free and MIT-licensed forever*

## License

MIT