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 theirGET …/{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
| Route | Purpose |
|---|---|
GET /mcp | Server name, version, uptime |
GET /mcp/agent-card | ERC-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.methodslists only the routes whose backing provider is attached right now.protocols.mcp.unavailable_methodslists the rest, so a client can tell "not wired" from "never existed".- Each entry in
capabilitiescarries anavailableflag (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
| Route | Query parameters | Returns |
|---|---|---|
GET /mcp/status | – | Operational status (running / paused / degraded) |
GET /mcp/portfolio | – | Net open positions |
GET /mcp/performance | period | Historical metrics |
GET /mcp/trades | period, symbol, limit | Closed trade history |
GET /mcp/orders | symbol | Resting 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/markets | symbol | Price / ATR / funding snapshot per supported pair |
GET /mcp/trades
| Parameter | Values | Default |
|---|---|---|
period | 24h, 7d, 30d, all | 7d |
symbol | e.g. BTC/USDC, ETH, xyz:TSLA (namespaces are matched case-insensitively and never upper-cased away) | all symbols |
limit | 1–500; larger values are clamped to 500, non-numeric values fall back to 50 | 50 |
{
"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"
}
]
}
sourcetells 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, soentry_priceandhold_duration_secondsarenullandexit_reasonis"unknown".completeis about the history window, notlimit. Fills are fetched from the newest end backwards, so the rows you get are always the most recent ones;complete: false(withincomplete_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.limitcaps 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/Border sides are normalised tosell/buy; millisecond timestamps become UTC ISO-8601. - The order fetch runs in strict mode: if the exchange call fails the route answers
502instead of an empty200, so an outage can never look like "no protective orders resting".fail_closed: falsemeans 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
NaNand±Infinity), isnull, listed inmissing_fields, and the whole response is markeddegraded: true. No defaults are substituted; the daily-loss limit is whatever the agent's effective risk configuration says, never a placeholder. daily_pnl_remaining_usdis only computed when bothdaily_pnl_usdandmax_daily_loss_usdare 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 staysfalsebut is reported inmissing_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
| Field | Type | Notes |
|---|---|---|
amount | integer | USDC in 6-decimal units; minimum 100000000 (100 USDC) |
network | string | arbitrum or base |
depositor_address | string | required |
x402_payment_id | string | optional |
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
| Field | Type | Notes |
|---|---|---|
amount | integer | USDC in 6-decimal units; must be positive |
network | string | arbitrum or base |
recipient_address | string | required |
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
| Code | Meaning |
|---|---|
200 | Success. Check degraded / complete where present. |
400 | Invalid query parameter (for example an unknown period). |
502 | The backing provider is attached but the fetch failed or returned malformed data. Treat the data you do not have as unknown. |
503 | The backing provider is not attached (the route is listed under unavailable_methods) or the request timed out. |
500 | Unexpected handler error. |
201 | A POST /mcp/deposit or POST /mcp/withdraw request was accepted. |
501 | Not 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.