Skip to main content

MCP Server

Owlpha exposes a small Model Context Protocol surface so that other agents, dashboards and auditors can see what the agent is doing. It has two parts:

  • Read-only data routes (GET): status, portfolio, performance, and the five depth routes documented below (/mcp/trades, /mcp/orders, /mcp/signals, /mcp/risk, /mcp/markets). None of these can place, cancel or modify an order, and the safety-critical trading write paths (order placement, position closes, risk limits) are deliberately not exposed over MCP at all.
  • Fund-movement routes (POST /mcp/deposit, POST /mcp/withdraw, plus their GET …/{id} status lookups) for the x402 escrow flow. These do not touch trading either; they hand back the parameters for an on-chain escrow transaction. See Fund movement.

The base URL is advertised in the agent card as protocols.mcp.endpoint (production: https://owlpha.7n7d.com/mcp).

Discovery​

RoutePurpose
GET /mcpServer name, version, uptime
GET /mcp/agent-cardERC-8004 agent card: capabilities, protocols, payments, fees

Only attached routes are advertised​

The agent card is built from the live wiring of the server, not from a static list:

  • protocols.mcp.methods lists only the routes whose backing provider is attached right now.
  • protocols.mcp.unavailable_methods lists the rest, so a client can tell "not wired" from "never existed".
  • Each entry in capabilities carries an available flag (true/false).

The A2A discover and capabilities methods use the same provider map, so both cards always agree. A route that is not advertised answers 503 (see below).

Depth routes​

RouteQuery parametersReturns
GET /mcp/status–Operational status (running / paused / degraded)
GET /mcp/portfolio–Net open positions
GET /mcp/performanceperiodHistorical metrics
GET /mcp/tradesperiod, symbol, limitClosed trade history
GET /mcp/orderssymbolResting orders, including protective TP/SL
GET /mcp/signals–Currently active trading signals (curated excerpts, never raw model output)
GET /mcp/risk–Current risk posture
GET /mcp/marketssymbolPrice / ATR / funding snapshot per supported pair

GET /mcp/trades​

ParameterValuesDefault
period24h, 7d, 30d, all7d
symbole.g. BTC/USDC, ETH, xyz:TSLA (namespaces are matched case-insensitively and never upper-cased away)all symbols
limit1–500; larger values are clamped to 500, non-numeric values fall back to 5050
{
"period": "7d",
"symbol": null,
"source": "exchange_fills",
"trade_count": 2,
"limit": 50,
"complete": true,
"incomplete_reason": null,
"trades": [
{
"trade_id": "77",
"symbol": "BTC/USDC",
"side": "long",
"size": 0.1,
"entry_price": null,
"exit_price": 51000.0,
"realized_pnl_usd": 100.0,
"hold_duration_seconds": null,
"exit_reason": "unknown",
"fees_usd": 1.2,
"filled_at": "2026-09-21T14:13:20+00:00"
}
]
}
  • source tells you where the rows came from: a dedicated trade-history store (trade_history_client), an exchange client with round-trip records (exchange_closed_trades), or the exchange's closing fills (exchange_fills). Fill-derived rows only know the closing side of the round-trip, so entry_price and hold_duration_seconds are null and exit_reason is "unknown".
  • complete is about the history window, not limit. Fills are fetched from the newest end backwards, so the rows you get are always the most recent ones; complete: false (with incomplete_reason: "fill_history_truncated") means older fills in the requested period were omitted, for example because the paging cap was reached or a capped recent-fills fetch stopped before the period cutoff. A truncated history is never presented as complete.
  • limit caps the rows actually returned, regardless of how many the source hands back.

GET /mcp/orders​

{
"symbol": null,
"order_count": 1,
"fail_closed": true,
"orders": [
{
"oid": "12345",
"symbol": "BTC",
"side": "sell",
"type": "limit",
"price": 52000.0,
"size": 0.1,
"status": "open",
"reduce_only": true,
"created_at": "2026-09-21T14:13:20+00:00"
}
]
}
  • Hyperliquid's A/B order sides are normalised to sell/buy; millisecond timestamps become UTC ISO-8601.
  • The order fetch runs in strict mode: if the exchange call fails the route answers 502 instead of an empty 200, so an outage can never look like "no protective orders resting". fail_closed: false means the attached exchange client cannot fail closed and an empty list should be treated with suspicion.

GET /mcp/risk​

{
"drawdown_usd": 25.0,
"daily_pnl_usd": -40.0,
"daily_pnl_remaining_usd": 60.0,
"daily_trade_count": 1,
"leverage_avg": 4.0,
"circuit_breaker_tripped": false,
"max_daily_loss_usd": 100.0,
"capital_deployed_pct": 18.0,
"degraded": false,
"missing_fields": [],
"updated_at": "2026-09-21T14:13:20+00:00"
}

The risk view is designed so that missing data can never look healthy:

  • Every numeric field is nullable. A value the risk monitor did not report, or reported as something unparsable (including NaN and ±Infinity), is null, listed in missing_fields, and the whole response is marked degraded: true. No defaults are substituted; the daily-loss limit is whatever the agent's effective risk configuration says, never a placeholder.
  • daily_pnl_remaining_usd is only computed when both daily_pnl_usd and max_daily_loss_usd are known. A profitable day does not increase it; only losses shrink it.
  • circuit_breaker_tripped: a present non-boolean value is treated as tripped (fail closed); a missing value stays false but is reported in missing_fields.
  • If the risk monitor returns something that is not an object at all, the route answers 502.

GET /mcp/markets and GET /mcp/signals​

Market snapshots carry last_price, change_24h_pct, atr_24h and optional funding_rate / volume_24h_usd. updated_at is null when the provider did not say when the snapshot was taken; the server never substitutes "now", so a stale price cannot pass a freshness check.

Signals carry symbol, direction (long / short / neutral), confidence (clamped to 0–1), source, an optional reasoning_excerpt (curated, truncated to 500 characters) and acted_on.

Fund movement (POST routes)​

These are the only non-GET routes. They are part of the x402 escrow flow described in the agent card (payments.x402) and never touch the trading engine: the server validates the request and returns the parameters for a transaction you then send on-chain to the escrow contract. Requests are JSON bodies; there is no bearer authentication on these routes today, so treat them as public and rely on the on-chain contract (which holds the funds, the 30-day escrow and the 24-hour cancel window) for enforcement.

POST /mcp/deposit​

FieldTypeNotes
amountintegerUSDC in 6-decimal units; minimum 100000000 (100 USDC)
networkstringarbitrum or base
depositor_addressstringrequired
x402_payment_idstringoptional

Returns 201 with deposit_id, status (pending), amount (as a string), network, escrow_contract (the address to call depositWithEscrow() on), release_time (30 days), cancel_deadline (24 hours), transaction_hash (null until you submit) and a message. Errors: 400 (invalid_network, invalid_amount, missing depositor_address, bad JSON), 503 (network_not_configured when no escrow contract is deployed for that network). GET /mcp/deposit/{id}?network=… reads the on-chain deposit state through the escrow manager.

POST /mcp/withdraw​

FieldTypeNotes
amountintegerUSDC in 6-decimal units; must be positive
networkstringarbitrum or base
recipient_addressstringrequired

Returns 201 with withdraw_id, status (pending, or queued for withdrawals above 50,000 USDC, which take the 7-day path), amount, network, recipient_address, estimated_completion, queue_position and a message. Errors: 400 (invalid_network, invalid_amount, missing recipient_address, bad JSON). GET /mcp/withdraw/{id} currently answers 501 not_implemented; withdrawal status tracking is not wired yet.

Status codes​

CodeMeaning
200Success. Check degraded / complete where present.
400Invalid query parameter (for example an unknown period).
502The backing provider is attached but the fetch failed or returned malformed data. Treat the data you do not have as unknown.
503The backing provider is not attached (the route is listed under unavailable_methods) or the request timed out.
500Unexpected handler error.
201A POST /mcp/deposit or POST /mcp/withdraw request was accepted.
501Not implemented yet (currently GET /mcp/withdraw/{id}).

The distinction matters for automation: a 503 is a wiring/availability fact you can discover up front from the agent card, while a 502 is a transient exchange or provider failure worth retrying.

Timestamps​

All timestamps are ISO-8601. Values that arrive as millisecond epochs from the exchange are converted to timezone-aware UTC (+00:00); values recorded by the agent in naive UTC carry a trailing Z. Unknown timestamps are null.