From baefa9c90561db6c7cc9442c3d8f17cd6fd7e541 Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Wed, 1 Jul 2026 12:40:55 -0400 Subject: [PATCH 1/5] docs/deploy cleanup: skill.md, RUN_MODE=mcp, market map Sync cli/skill.md and api-reference.md with 19 strategies and ~21 MCP tools; replace stale DSL refs with hl guard; document BTCSWP osrs/para/yex matrix. Default Railway/entrypoint RUN_MODE to mcp; extend market_strategy_map for BTCSWP-OSRS/PARA with tests. Co-authored-by: Cursor --- README.md | 4 ++-- cli/skill.md | 34 ++++++++++++++++++--------- docs/api-reference.md | 29 ++++++++++------------- modules/market_strategy_map.py | 2 ++ railway.toml | 2 +- scripts/entrypoint.py | 10 ++++---- tests/test_entrypoint.py | 6 +++++ tests/test_market_strategy_routing.py | 12 ++++++++++ tests/test_strategy_registry.py | 4 ++++ 9 files changed, 67 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index f1d1a3b..ad21331 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Strategies Tests License - MCP + MCP

@@ -550,7 +550,7 @@ hl mcp serve # stdio transport (default) hl mcp serve --transport sse # SSE transport ``` -**20 MCP tools** for account state, trading, APEX/Radar/REFLECT, wallet/setup, safety actions (`schedule_cancel`, `emergency_close_all`), and agent memory/journal helpers. Run `hl mcp serve` to expose them to any MCP host. +**21 MCP tools** for account state, trading, APEX/Radar/REFLECT, wallet/setup, safety actions (`schedule_cancel`, `emergency_close_all`), and agent memory/journal helpers. Run `hl mcp serve` to expose them to any MCP host. Fast tools (strategies, builder, wallet, setup, memory, journal, judge) call Python directly — zero subprocess overhead. diff --git a/cli/skill.md b/cli/skill.md index d227efa..855cce4 100644 --- a/cli/skill.md +++ b/cli/skill.md @@ -1,6 +1,6 @@ --- name: yex-trader -description: Autonomous Hyperliquid trading — 14 strategies (MM, momentum, arbitrage, LLM) with APEX multi-slot orchestrator, REFLECT performance review, DSL trailing stops, and builder fee revenue collection. +description: Autonomous Hyperliquid trading — 19 strategies (MM, momentum, arbitrage, LLM) with APEX multi-slot orchestrator, REFLECT performance review, Guard trailing stops, and builder fee revenue collection. user-invocable: true argument-hint: " [options]" allowed-tools: @@ -17,7 +17,7 @@ metadata: # YEX Trader -Autonomous Hyperliquid trading via agent-cli. 14 strategies across market making, momentum, arbitrage, and LLM-powered trading. APEX multi-slot orchestrator. REFLECT nightly performance review. Builder fee revenue collection. +Autonomous Hyperliquid trading via agent-cli. 19 strategies across market making, momentum, arbitrage, and LLM-powered trading. APEX multi-slot orchestrator. REFLECT nightly performance review. Builder fee revenue collection. ## Quick Start (Agent-Friendly) @@ -137,13 +137,12 @@ hl reflect report [--date 2026-03-03] hl reflect history [-n 10] ``` -### Dynamic Stop Loss (DSL) +### Guard Trailing Stop ```bash -hl dsl start [--entry-price 2500] [--direction long] [--preset tight|standard|wide] -hl dsl check -hl dsl status -hl dsl presets +hl guard start ETH-PERP --entry 2500 --size 1 --direction long [--preset tight|moderate] +hl guard status +hl guard presets ``` ### Radar & Movers @@ -179,16 +178,16 @@ hl setup bootstrap # Auto-create venv and install hl setup claim-usdyp # Claim testnet USDyP tokens ``` -### MCP Server (16 Tools) +### MCP Server (~21 Tools) ```bash hl mcp serve # Start MCP server (stdio transport) hl mcp serve --transport sse # Start MCP server (SSE transport) ``` -Tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_check`, `account`, `status`, `trade`, `run_strategy`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `agent_memory`, `trade_journal`, `judge_report` +Tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_check`, `account`, `status`, `trade`, `run_strategy`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `schedule_cancel`, `emergency_close_all`, `order_status`, `funding_rates`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context` -## Strategies (14) +## Strategies (19) | Name | Type | Description | |------|------|-------------| @@ -204,13 +203,26 @@ Tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_chec | momentum_breakout | Signal | Enters on volume + price breakout above/below N-period range | | aggressive_taker | Taker | Directional spread crossing with bias | | hedge_agent | Risk | Reduces excess exposure per deterministic mandate | +| cfi_hedge | Risk | CFI-v2 funding-cost hedge (YEX testnet / Paragon mainnet BTCSWP) | | rfq_agent | RFQ | Block-size dark RFQ liquidity | | claude_agent | LLM | Claude/Gemini-powered autonomous trading agent | +| simplified_ensemble | Signal | 6-signal ensemble vote | +| funding_momentum | Signal | Funding rate mean-reversion with EMA confirmation | +| oi_divergence | Signal | Price/OI divergence filter | +| trend_follower | Signal | EMA crossover + ADX trend strength filter | ## Instruments - **Standard perps**: ETH-PERP, BTC-PERP, SOL-PERP, etc. -- **YEX yield markets**: VXX-USDYP (yex:VXX), US3M-USDYP (yex:US3M) +- **YEX yield markets (testnet)**: VXX-USDYP (`yex:VXX`), US3M-USDYP (`yex:US3M`), BTCSWP-USDYP (`yex:BTCSWP`) +- **Paragon BTCSWP swap perps (HIP-3)**: + +| Network | Instrument | HL coin | Notes | +|---------|------------|---------|-------| +| Testnet | `BTCSWP-OSRS` | `osrs:BTCSWP` | Paragon swap perp on the `osrs` dex | +| Mainnet | `BTCSWP-PARA` | `para:BTCSWP` | Paragon swap perp on the `para` dex | + +Shorthand `BTCSWP` resolves by network: testnet → `BTCSWP-USDYP` (YEX yield), mainnet → `BTCSWP-PARA`. Use `BTCSWP-OSRS` or `osrs:BTCSWP` for the explicit Paragon swap perp on testnet. ## Workflow diff --git a/docs/api-reference.md b/docs/api-reference.md index a746466..65193e4 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -221,7 +221,9 @@ curl $AGENT_URL/api/strategies "markets": { "VXX-USDYP": "Volatility index yield perpetual", "US3M-USDYP": "US 3-month Treasury rate yield perpetual", - "BTCSWP-USDYP": "BTC interest rate swap yield perpetual" + "BTCSWP-USDYP": "BTC interest rate swap yield perpetual (YEX testnet)", + "BTCSWP-OSRS": "BTC interest rate swap perp (OSRS testnet)", + "BTCSWP-PARA": "BTC interest rate swap perp (Paragon mainnet)" } } ``` @@ -267,8 +269,8 @@ curl -X POST $AGENT_URL/api/skill/install \ ```json { "installed": true, - "strategies": 14, - "tools": 13 + "strategies": 19, + "tools": 21 } ``` @@ -575,24 +577,13 @@ async function fetchLeaderboard(network = 'testnet') { setInterval(() => fetchLeaderboard().then(renderTable), 30000); ``` -**CLI usage:** - -```bash -# Register an address -python leaderboard.py register 0x1234...abcd --name "my-agent" --network testnet - -# List current rankings -python leaderboard.py list --network testnet - -# Start the HTTP server -python leaderboard.py serve --port 8090 -``` +**CLI usage:** Use the HTTP endpoints above (`/api/register`, `/api/leaderboard`). This repo does not ship a `leaderboard.py` CLI — deploy the separate leaderboard microservice from the cli-UI repo. --- ## MCP Server -The MCP server exposes 16 tools for AI agent orchestration via the [Model Context Protocol](https://modelcontextprotocol.io). This is the access path for Claude Code, OpenClaw, or any MCP-compatible client. +The MCP server exposes 21 tools for AI agent orchestration via the [Model Context Protocol](https://modelcontextprotocol.io). This is the access path for Claude Code, OpenClaw, or any MCP-compatible client. ### Starting the Server @@ -627,7 +618,7 @@ These execute directly in Python with no subprocess overhead. #### `strategies()` -List all 14 trading strategies with descriptions and default parameters. +List all 19 trading strategies with descriptions and default parameters. ``` Tool: strategies @@ -1060,6 +1051,10 @@ else: | `apex_status` | Subprocess | <1s | None | | `apex_run` | Subprocess | Minutes+ | Runs APEX loop | | `reflect_run` | Subprocess | 5-15s | None | +| `schedule_cancel` | Subprocess | 1-5s | Cancels open orders on HL | +| `emergency_close_all` | Subprocess | 1-10s | Closes all positions on HL | +| `order_status` | Subprocess | 1-5s | None | +| `funding_rates` | Subprocess | 1-5s | None | | `agent_memory` | Fast | <100ms | None | | `trade_journal` | Fast | <100ms | None | | `judge_report` | Fast | <100ms | None | diff --git a/modules/market_strategy_map.py b/modules/market_strategy_map.py index 4c29516..320d582 100644 --- a/modules/market_strategy_map.py +++ b/modules/market_strategy_map.py @@ -6,6 +6,8 @@ MARKET_STRATEGY_MAP: Dict[str, List[str]] = { "VXX-USDYP": ["mean_reversion", "simplified_ensemble"], "BTCSWP-USDYP": ["funding_arb", "funding_momentum", "basis_arb"], + "BTCSWP-OSRS": ["funding_arb", "funding_momentum", "basis_arb"], + "BTCSWP-PARA": ["funding_arb", "funding_momentum", "basis_arb"], "US3M-USDYP": ["trend_follower", "simplified_ensemble"], } diff --git a/railway.toml b/railway.toml index ef3cb55..64d10e2 100644 --- a/railway.toml +++ b/railway.toml @@ -13,6 +13,6 @@ mountPath = "/data" PORT = "8080" HL_TESTNET = "true" APEX_PRESET = "default" -RUN_MODE = "apex" +RUN_MODE = "mcp" INSTRUMENT = "ETH-PERP" DATA_DIR = "/data" diff --git a/scripts/entrypoint.py b/scripts/entrypoint.py index d395b86..852973d 100644 --- a/scripts/entrypoint.py +++ b/scripts/entrypoint.py @@ -66,7 +66,7 @@ def do_GET(self): if self.path == "/health": body = json.dumps({ "status": "ok", - "mode": os.environ.get("RUN_MODE", "apex"), + "mode": os.environ.get("RUN_MODE", "mcp"), "uptime_s": int(time.time() - START_TIME), "pid": CHILD_PROC.pid if CHILD_PROC else None, "alive": runner_alive(), @@ -225,7 +225,7 @@ def do_POST(self): from cli.api.status_reader import read_strategies data = read_strategies() count = len(data.get("strategies", {})) - self._json_response(json.dumps({"installed": True, "strategies": count, "tools": 13}), cors=True) + self._json_response(json.dumps({"installed": True, "strategies": count, "tools": len(MCP_ALL_TOOLS)}), cors=True) except Exception as e: self.send_response(500) self._send_cors_headers() @@ -299,7 +299,7 @@ def log_message(self, format, *args): def build_command() -> list[str]: """Build the CLI command from environment variables.""" - mode = os.environ.get("RUN_MODE", "apex").lower() + mode = os.environ.get("RUN_MODE", "mcp").lower() py = [sys.executable, "-m", "cli.main"] if mode in ("apex", "wolf"): @@ -357,7 +357,7 @@ def build_command() -> list[str]: def runner_alive() -> bool: if CHILD_PROC is not None: return CHILD_PROC.poll() is None - return os.environ.get("RUN_MODE", "apex").lower() == "mcp" + return os.environ.get("RUN_MODE", "mcp").lower() == "mcp" def handle_mcp_json_rpc(raw_body: bytes, headers: Any) -> tuple[int, dict[str, Any]]: @@ -723,7 +723,7 @@ class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): except Exception: pass # best-effort - mode = os.environ.get("RUN_MODE", "apex") + mode = os.environ.get("RUN_MODE", "mcp") if mode.lower() == "mcp": log.info("Starting mcp mode: HTTP JSON-RPC wrapper active on /mcp/trading") while True: diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index f12cd12..c89dfee 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -21,6 +21,12 @@ # --------------------------------------------------------------------------- class TestBuildCommand: + def test_default_mode_is_mcp(self, monkeypatch): + monkeypatch.delenv("RUN_MODE", raising=False) + + cmd = build_command() + assert cmd == [sys.executable, "-m", "cli.main", "mcp", "serve", "--transport", "sse"] + def test_apex_mode_default(self, monkeypatch): monkeypatch.setenv("RUN_MODE", "apex") monkeypatch.delenv("APEX_PRESET", raising=False) diff --git a/tests/test_market_strategy_routing.py b/tests/test_market_strategy_routing.py index da597c0..f8067e7 100644 --- a/tests/test_market_strategy_routing.py +++ b/tests/test_market_strategy_routing.py @@ -16,6 +16,18 @@ def test_vxx_mapping(self): assert "mean_reversion" in strats assert "simplified_ensemble" in strats + def test_btcswp_osrs_mapping(self): + strats = get_strategies_for_market("BTCSWP-OSRS") + assert "funding_arb" in strats + assert "funding_momentum" in strats + assert "basis_arb" in strats + + def test_btcswp_para_mapping(self): + strats = get_strategies_for_market("BTCSWP-PARA") + assert "funding_arb" in strats + assert "funding_momentum" in strats + assert "basis_arb" in strats + def test_btcswp_mapping(self): strats = get_strategies_for_market("BTCSWP-USDYP") assert "funding_arb" in strats diff --git a/tests/test_strategy_registry.py b/tests/test_strategy_registry.py index 9b64de0..565ec3d 100644 --- a/tests/test_strategy_registry.py +++ b/tests/test_strategy_registry.py @@ -61,6 +61,10 @@ def test_yex_btcswp_reverse_lookup(self): + def test_osrs_btcswp_reverse_lookup(self): + assert resolve_instrument("osrs:BTCSWP", mainnet=False) == "BTCSWP-OSRS" + assert resolve_instrument("BTCSWP-OSRS", mainnet=False) == "BTCSWP-OSRS" + def test_para_btcswp_reverse_lookup(self): assert resolve_instrument("para:BTCSWP", mainnet=True) == "BTCSWP-PARA" assert resolve_instrument("BTCSWP-PARA", mainnet=True) == "BTCSWP-PARA" From 23396e03effd2835c180c1970bbf5767e50329bf Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Wed, 1 Jul 2026 13:41:43 -0400 Subject: [PATCH 2/5] feat: expose confirmed funding hedge MCP execution Co-authored-by: Cursor --- README.md | 4 +- cli/mcp_server.py | 87 +++++++++++++++++++++- cli/skill.md | 4 +- deploy/hermes-railway/workspace/TOOLS.md | 6 +- deploy/openclaw-railway/workspace/TOOLS.md | 6 +- docs/api-reference.md | 3 + scripts/entrypoint.py | 45 +++++++++++ tests/test_mcp_annotations.py | 21 +++++- tests/test_mcp_gateway_context.py | 62 +++++++++++++++ 9 files changed, 227 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ad21331..d77b805 100644 --- a/README.md +++ b/README.md @@ -550,7 +550,7 @@ hl mcp serve # stdio transport (default) hl mcp serve --transport sse # SSE transport ``` -**21 MCP tools** for account state, trading, APEX/Radar/REFLECT, wallet/setup, safety actions (`schedule_cancel`, `emergency_close_all`), and agent memory/journal helpers. Run `hl mcp serve` to expose them to any MCP host. +**24 MCP tools** for account state, trading, CFI funding hedges, APEX/Radar/REFLECT, wallet/setup, safety actions (`schedule_cancel`, `emergency_close_all`), and agent memory/journal helpers. Run `hl mcp serve` to expose them to any MCP host. Fast tools (strategies, builder, wallet, setup, memory, journal, judge) call Python directly — zero subprocess overhead. @@ -619,7 +619,7 @@ Railway build root at the repo root. ``` cli/ CLI commands and trading engine commands/ Subcommand modules (run, apex, radar, pulse, guard, reflect, house, ...) - mcp_server.py MCP server (20 tools via FastMCP) + mcp_server.py MCP server (24 tools via FastMCP) hl_adapter.py Direct HL API adapter (live + mock) builder_fee.py Builder fee config (HL native BuilderInfo) keystore.py Encrypted keystore (geth-compatible) diff --git a/cli/mcp_server.py b/cli/mcp_server.py index f59d31d..b37aece 100644 --- a/cli/mcp_server.py +++ b/cli/mcp_server.py @@ -28,12 +28,14 @@ _READ_ONLY_TOOLS = { "strategies", "builder_status", "wallet_list", "setup_check", "account", "status", "apex_status", + "funding_hedge_propose", "funding_hedge_backtest", "agent_memory", "trade_journal", "judge_report", "obsidian_context", "order_status", "funding_rates", } # Tools that move funds or cancel/close live orders/positions — handle with care. _DESTRUCTIVE_TOOLS = { - "trade", "run_strategy", "apex_run", "schedule_cancel", "emergency_close_all", + "trade", "run_strategy", "apex_run", "funding_hedge_execute", + "schedule_cancel", "emergency_close_all", } # Everything else (wallet_auto, radar_run, reflect_run) is # state-changing-but-safe: neither a pure read nor fund-destructive. @@ -357,8 +359,9 @@ def _ann(name: str, title: str): instructions=( "Autonomous Hyperliquid trading CLI — 14 strategies, APEX orchestrator, " "REFLECT reviews. Always confirm details with the user before calling " - "destructive tools (trade, run_strategy, apex_run, schedule_cancel, " - "emergency_close_all). " + "destructive tools (trade, run_strategy, apex_run, funding_hedge_execute, " + "schedule_cancel, emergency_close_all). " + "funding_hedge_execute requires confirmed=true. " "emergency_close_all requires confirm=true." ), ) @@ -796,6 +799,84 @@ def funding_rates( args.append("--mainnet") return _run_hl(*args, env_overrides=_request_env(ctx)) + @mcp.tool(**_ann("funding_hedge_propose", "Funding hedge proposal")) + def funding_hedge_propose( + coin: str = "BTC", + mainnet: bool = False, + ctx: FastMCPContext = None, + ) -> str: + """Build a CFI v2 funding hedge proposal without placing an order. + + Args: + coin: Perp coin to hedge (for example BTC). + mainnet: Use mainnet instead of testnet. + """ + args = ["hedge", "propose", coin] + if mainnet: + args.append("--mainnet") + return _run_hl(*args, timeout=60, env_overrides=_request_env(ctx)) + + @mcp.tool(**_ann("funding_hedge_backtest", "Funding hedge backtest")) + def funding_hedge_backtest( + coin: str = "BTC", + days: int = 365, + notional: float = 1_000_000, + ctx: FastMCPContext = None, + ) -> str: + """Run the reference CFI v2 funding hedge backtest. + + Args: + coin: Perp coin to backtest (for example BTC). + days: Backtest window; passed through for CLI compatibility. + notional: Source perp notional in USD. + """ + return _run_hl( + "hedge", "backtest", + "--coin", coin, + "--days", str(days), + "--notional", str(notional), + timeout=120, + env_overrides=_request_env(ctx), + ) + + @mcp.tool(**_ann("funding_hedge_execute", "Execute funding hedge")) + def funding_hedge_execute( + coin: str = "BTC", + dry_run: bool = False, + mainnet: bool = False, + confirmed: bool = False, + ctx: FastMCPContext = None, + ) -> str: + """Build and execute a CFI v2 funding hedge through `hl hedge execute`. + + WARNING: with dry_run=False this can place a real CFI v2 hedge order. + + Args: + coin: Perp coin to hedge (for example BTC). + dry_run: Preview the order only; no submit and no hedge-state write. + mainnet: Use mainnet instead of testnet. + confirmed: Must be true after explicit user approval. + """ + if not confirmed: + return _json_error("funding_hedge_execute requires confirmed=true after explicit user approval.") + env_overrides = _request_env(ctx) + error = _context_limit_error( + "funding_hedge_execute", + env_overrides, + mainnet=mainnet, + confirmed=confirmed, + require_signing=True, + ) + if error: + return _json_error(error) + args = ["hedge", "execute", coin] + if dry_run: + args.append("--dry-run") + if mainnet: + args.append("--mainnet") + args.append("--yes") + return _run_hl(*args, timeout=120, env_overrides=env_overrides) + # ------------------------------------------------------------------ # Self-improvement tools — memory, journal, judge, obsidian # ------------------------------------------------------------------ diff --git a/cli/skill.md b/cli/skill.md index 855cce4..7807ca5 100644 --- a/cli/skill.md +++ b/cli/skill.md @@ -178,14 +178,14 @@ hl setup bootstrap # Auto-create venv and install hl setup claim-usdyp # Claim testnet USDyP tokens ``` -### MCP Server (~21 Tools) +### MCP Server (~24 Tools) ```bash hl mcp serve # Start MCP server (stdio transport) hl mcp serve --transport sse # Start MCP server (SSE transport) ``` -Tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_check`, `account`, `status`, `trade`, `run_strategy`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `schedule_cancel`, `emergency_close_all`, `order_status`, `funding_rates`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context` +Tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_check`, `account`, `status`, `trade`, `run_strategy`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `schedule_cancel`, `emergency_close_all`, `order_status`, `funding_rates`, `funding_hedge_propose`, `funding_hedge_backtest`, `funding_hedge_execute`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context` ## Strategies (19) diff --git a/deploy/hermes-railway/workspace/TOOLS.md b/deploy/hermes-railway/workspace/TOOLS.md index 94d1f2e..f065eb5 100644 --- a/deploy/hermes-railway/workspace/TOOLS.md +++ b/deploy/hermes-railway/workspace/TOOLS.md @@ -2,7 +2,7 @@ ## MCP Server: nunchi_trading -The primary tool provider, registered in `config.yaml` under `mcp_servers.nunchi_trading`. Exposes 13 trading tools via Model Context Protocol: +The primary tool provider, registered in `config.yaml` under `mcp_servers.nunchi_trading`. Exposes 24 trading tools via Model Context Protocol, including: - `account` — Show HL account state (balance, margin, positions) - `status` — Current positions, PnL, and risk state @@ -17,6 +17,10 @@ The primary tool provider, registered in `config.yaml` under `mcp_servers.nunchi - `builder_status` — Check builder fee approval status - `wallet_list` — List available wallets - `wallet_auto` — Create wallet automatically +- `funding_rates` — Read current funding rates +- `funding_hedge_propose` — Build a CFI v2 funding hedge proposal +- `funding_hedge_backtest` — Run the reference funding hedge backtest +- `funding_hedge_execute` — Execute or dry-run a CFI v2 hedge; requires `confirmed=true` ## CLI: hl diff --git a/deploy/openclaw-railway/workspace/TOOLS.md b/deploy/openclaw-railway/workspace/TOOLS.md index a5a5cc5..49964cb 100644 --- a/deploy/openclaw-railway/workspace/TOOLS.md +++ b/deploy/openclaw-railway/workspace/TOOLS.md @@ -11,7 +11,7 @@ mcporter call nunchi_trading.account # call a tool (no args) mcporter call nunchi_trading.trade instrument=ETH-PERP side=buy size=0.1 ``` -Exposes 13 trading tools via Model Context Protocol: +Exposes 24 trading tools via Model Context Protocol, including: - `account` — Show HL account state (balance, margin, positions) - `status` — Current positions, PnL, and risk state @@ -26,6 +26,10 @@ Exposes 13 trading tools via Model Context Protocol: - `builder_status` — Check builder fee approval status - `wallet_list` — List available wallets - `wallet_auto` — Create wallet automatically +- `funding_rates` — Read current funding rates +- `funding_hedge_propose` — Build a CFI v2 funding hedge proposal +- `funding_hedge_backtest` — Run the reference funding hedge backtest +- `funding_hedge_execute` — Execute or dry-run a CFI v2 hedge; requires `confirmed=true` ## CLI: hl diff --git a/docs/api-reference.md b/docs/api-reference.md index 65193e4..9ae7dd3 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1055,6 +1055,9 @@ else: | `emergency_close_all` | Subprocess | 1-10s | Closes all positions on HL | | `order_status` | Subprocess | 1-5s | None | | `funding_rates` | Subprocess | 1-5s | None | +| `funding_hedge_propose` | Subprocess | 1-10s | None | +| `funding_hedge_backtest` | Subprocess | 1-120s | None | +| `funding_hedge_execute` | Subprocess | 1-120s | Places a CFI v2 hedge order unless `dry_run=true`; requires `confirmed=true` | | `agent_memory` | Fast | <100ms | None | | `trade_journal` | Fast | <100ms | None | | `judge_report` | Fast | <100ms | None | diff --git a/scripts/entrypoint.py b/scripts/entrypoint.py index 852973d..c40832a 100644 --- a/scripts/entrypoint.py +++ b/scripts/entrypoint.py @@ -44,12 +44,15 @@ "obsidian_context", "order_status", "funding_rates", + "funding_hedge_propose", + "funding_hedge_backtest", } MCP_WRITE_TOOLS = { "wallet_auto", "trade", "run_strategy", "apex_run", + "funding_hedge_execute", "schedule_cancel", "emergency_close_all", } @@ -449,6 +452,24 @@ def call_mcp_tool(name: str, arguments: dict[str, Any], headers: Any) -> str: if _bool_arg(arguments, "mainnet"): cmd.append("--mainnet") return _run_hl(*cmd, env_overrides=env_overrides) + if name == "funding_hedge_propose": + coin = _str_arg(arguments, "coin") or "BTC" + cmd = ["hedge", "propose", coin] + if _bool_arg(arguments, "mainnet"): + cmd.append("--mainnet") + return _run_hl(*cmd, timeout=60, env_overrides=env_overrides) + if name == "funding_hedge_backtest": + coin = _str_arg(arguments, "coin") or "BTC" + days = _int_arg(arguments, "days") or 365 + notional = _float_arg(arguments, "notional") or 1_000_000 + return _run_hl( + "hedge", "backtest", + "--coin", coin, + "--days", str(days), + "--notional", str(notional), + timeout=120, + env_overrides=env_overrides, + ) if name == "agent_memory": return _agent_memory_text(arguments) if name == "trade_journal": @@ -542,6 +563,30 @@ def call_mcp_tool(name: str, arguments: dict[str, Any], headers: Any) -> str: cmd.append("--mainnet") return _run_hl(*cmd, timeout=max(120, (effective_max_ticks or 10) * 60 + 30), env_overrides=env_overrides) + if name == "funding_hedge_execute": + coin = _str_arg(arguments, "coin") or "BTC" + dry_run = _bool_arg(arguments, "dry_run") + mainnet = _bool_arg(arguments, "mainnet") + confirmed = _bool_arg(arguments, "confirmed") + if not confirmed: + return _json_error("funding_hedge_execute requires confirmed=true after explicit user approval.") + error = _context_limit_error( + "funding_hedge_execute", + env_overrides, + mainnet=mainnet, + confirmed=confirmed, + require_signing=True, + ) + if error: + return _json_error(error) + cmd = ["hedge", "execute", coin] + if dry_run: + cmd.append("--dry-run") + if mainnet: + cmd.append("--mainnet") + cmd.append("--yes") + return _run_hl(*cmd, timeout=120, env_overrides=env_overrides) + if name == "wallet_auto": return _json_error("wallet_auto is disabled on the hosted keyless runner") if name in {"schedule_cancel", "emergency_close_all"}: diff --git a/tests/test_mcp_annotations.py b/tests/test_mcp_annotations.py index 27baea2..bf868c6 100644 --- a/tests/test_mcp_annotations.py +++ b/tests/test_mcp_annotations.py @@ -13,13 +13,28 @@ def test_classification_sets_are_disjoint(): def test_destructive_set_covers_fund_movers(): from cli.mcp_server import _DESTRUCTIVE_TOOLS - for name in ("trade", "run_strategy", "apex_run", "schedule_cancel", "emergency_close_all"): + for name in ( + "trade", + "run_strategy", + "apex_run", + "funding_hedge_execute", + "schedule_cancel", + "emergency_close_all", + ): assert name in _DESTRUCTIVE_TOOLS def test_read_only_set_covers_reads(): from cli.mcp_server import _READ_ONLY_TOOLS - for name in ("account", "status", "strategies", "order_status", "funding_rates"): + for name in ( + "account", + "status", + "strategies", + "order_status", + "funding_rates", + "funding_hedge_propose", + "funding_hedge_backtest", + ): assert name in _READ_ONLY_TOOLS @@ -40,7 +55,9 @@ def test_server_applies_annotations(): assert by_name["trade"].annotations is not None assert by_name["trade"].annotations.destructiveHint is True assert by_name["trade"].annotations.readOnlyHint is False + assert by_name["funding_hedge_execute"].annotations.destructiveHint is True assert by_name["schedule_cancel"].annotations.destructiveHint is True assert by_name["emergency_close_all"].annotations.destructiveHint is True assert by_name["account"].annotations.readOnlyHint is True assert by_name["funding_rates"].annotations.readOnlyHint is True + assert by_name["funding_hedge_propose"].annotations.readOnlyHint is True diff --git a/tests/test_mcp_gateway_context.py b/tests/test_mcp_gateway_context.py index c57b0fc..e256583 100644 --- a/tests/test_mcp_gateway_context.py +++ b/tests/test_mcp_gateway_context.py @@ -187,3 +187,65 @@ def fake_run_hl(*args, timeout=30, env_overrides=None): assert captured["args"] == ("trade", "ETH-PERP", "buy", "0.1", "--yes") assert captured["env_overrides"]["NUNCHI_WEB_AUTH_PAIR_TOKEN"] == "pair-token" assert captured["env_overrides"]["NUNCHI_WEB_AUTH_ADDRESS"] == "0x" + "4" * 40 + + +def test_entrypoint_funding_hedge_execute_refuses_without_confirm(): + from scripts.entrypoint import handle_mcp_json_rpc + + body = json.dumps({ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "funding_hedge_execute", + "arguments": {"coin": "BTC", "dry_run": True}, + }, + }).encode() + + status, response = handle_mcp_json_rpc(body, {}) + + assert status == 200 + assert "confirmed=true" in response["result"]["content"][0]["text"] + + +def test_entrypoint_funding_hedge_execute_confirmed_dry_run_forwards_to_cli(monkeypatch, tmp_path): + import cli.mcp_server as mcp_server + from scripts.entrypoint import handle_mcp_json_rpc + + captured = {} + + def fake_run_hl(*args, timeout=30, env_overrides=None): + captured["args"] = args + captured["timeout"] = timeout + captured["env_overrides"] = env_overrides + return "hedge preview" + + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setenv("NUNCHI_RUNNER_CONTEXT_SECRET", "shared-secret") + monkeypatch.setattr(mcp_server, "_run_hl", fake_run_hl) + + body = json.dumps({ + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": { + "name": "funding_hedge_execute", + "arguments": {"coin": "BTC", "dry_run": True, "confirmed": True}, + }, + }).encode() + headers = { + "x-nunchi-secret-nunchi-runner-context-secret": "shared-secret", + "x-nunchi-secret-nunchi-web-auth-pair-token": "pair-token", + "x-nunchi-secret-nunchi-web-auth-address": "0x" + "5" * 40, + "x-nunchi-trading-permission-tier": "testnet_trading", + "x-nunchi-trading-network": "testnet", + } + + status, response = handle_mcp_json_rpc(body, headers) + + assert status == 200 + assert response["result"]["content"][0]["text"] == "hedge preview" + assert captured["args"] == ("hedge", "execute", "BTC", "--dry-run", "--yes") + assert captured["timeout"] == 120 + assert captured["env_overrides"]["NUNCHI_WEB_AUTH_PAIR_TOKEN"] == "pair-token" + assert captured["env_overrides"]["NUNCHI_WEB_AUTH_ADDRESS"] == "0x" + "5" * 40 From 59ba14423caf8a2674ddc30ea6bf1472c91828f3 Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Wed, 1 Jul 2026 14:18:48 -0400 Subject: [PATCH 3/5] feat: add MCP pricing measurement harness Co-authored-by: Cursor --- docs/MCP_PRICING_MEASUREMENTS.md | 52 +++++++ scripts/pricing_measure.py | 255 +++++++++++++++++++++++++++++++ tests/test_pricing_measure.py | 27 ++++ 3 files changed, 334 insertions(+) create mode 100644 docs/MCP_PRICING_MEASUREMENTS.md create mode 100644 scripts/pricing_measure.py create mode 100644 tests/test_pricing_measure.py diff --git a/docs/MCP_PRICING_MEASUREMENTS.md b/docs/MCP_PRICING_MEASUREMENTS.md new file mode 100644 index 0000000..0e997f8 --- /dev/null +++ b/docs/MCP_PRICING_MEASUREMENTS.md @@ -0,0 +1,52 @@ +# MCP Pricing Measurements + +Measured on 2026-07-01 for the hosted MCP tools runner path. + +## Harness + +Run: + +```bash +python3 scripts/pricing_measure.py --output tmp/pricing-measurement-local.json +railway run --service hosted-trading-mcp --environment production -- python3 scripts/pricing_measure.py --output tmp/pricing-measurement-runner-env.json +``` + +Use `--openrouter-live` only when spending OpenRouter credits is intended. + +## Results + +Production runner env dry-run: + +- `RUN_MODE=mcp`, `HL_TESTNET=true`. +- `python.import_cli`: 58.6 ms. +- MCP `strategies`: 118.7 ms, 3,068 response bytes. +- MCP `funding_hedge_execute` without `confirmed=true`: 0.09 ms refusal, no order path. +- Railway resource metrics, last 1h: `<0.01 vCPU`, 15 MB memory, 0 MB network, 0 MB disk. + +Local dry-run: + +- `python.import_cli`: 62.4 ms. +- MCP `strategies`: 120.8 ms. +- MCP `funding_hedge_execute` without `confirmed=true`: 0.10 ms refusal. + +## Economics + +Mode 1, hosted MCP tools: + +- `C_seat` is not computed yet. Railway metrics expose CPU/memory/network, but not monthly billing cost. Set `RAILWAY_SHARED_RUNTIME_MONTHLY_USD` or pass `--runtime-monthly-usd` once billing data is available. + +Mode 2, hosted MCP tools plus Nunchi/OpenRouter inference: + +- No live OpenRouter spend was measured because `OPENROUTER_API_KEY` is not present in the runner env and `--openrouter-live` was not run. +- Current inference budgets remain inputs only: Starter `$10`, Growth `$50`, Team `$250`. + +Mode 3, clone/local plus builder economics: + +- No funded-wallet fill measurement was run because the production runner has no `HL_PRIVATE_KEY`, `HL_KEYSTORE_PASSWORD`, or `~/.hl-agent/env`. +- Formulaic builder-fee economics at the default `BUILDER_FEE_TENTHS_BPS=100` are `$100` per `$100,000` notional and `$1,000` per `$1,000,000` notional. + +## Blockers + +- Missing Railway monthly billing cost input for Mode 1 `C_seat`. +- Missing `OPENROUTER_API_KEY` for Mode 2 inference spend. +- Missing funded-wallet/HL signing credentials for live fills and builder-fee realization. diff --git a/scripts/pricing_measure.py b/scripts/pricing_measure.py new file mode 100644 index 0000000..2bc7c43 --- /dev/null +++ b/scripts/pricing_measure.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +"""Measure MCP mode pricing inputs without fabricating missing live costs. + +Default execution is safe: it measures local/import and MCP JSON-RPC dry-run +latency, detects credential availability as booleans, and computes only formulas +whose inputs are explicit. Use --openrouter-live to spend OpenRouter credits. +""" +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +import time +import urllib.error +import urllib.request +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parent.parent +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + + +HOSTED_TOOLS_SEATS = { + "starter": 5, + "growth": 10, + "team": 50, +} + +HOSTED_INFERENCE_BUDGETS = { + "starter": 10.0, + "growth": 50.0, + "team": 250.0, +} + + +@dataclass(frozen=True) +class Measurement: + name: str + ok: bool + elapsed_ms: float + detail: dict[str, Any] + + +def env_flag(name: str) -> bool: + return bool(os.environ.get(name)) + + +def has_wallet_credentials() -> bool: + return any([ + env_flag("HL_PRIVATE_KEY"), + env_flag("HL_KEYSTORE_PASSWORD"), + Path(os.path.expanduser("~/.hl-agent/env")).exists(), + ]) + + +def builder_fee_rate_tenths_bps() -> int: + raw = os.environ.get("BUILDER_FEE_TENTHS_BPS", "100") + try: + value = int(raw) + except ValueError: + value = 100 + return max(value, 0) + + +def builder_revenue_usd(notional_usd: float, fee_tenths_bps: int) -> float: + return notional_usd * fee_tenths_bps / 100_000 + + +def runtime_c_seat(runtime_monthly_usd: float | None) -> dict[str, Any]: + if runtime_monthly_usd is None: + return { + "computed": False, + "blocker": "Set --runtime-monthly-usd or RAILWAY_SHARED_RUNTIME_MONTHLY_USD from Railway billing/metrics.", + } + return { + "computed": True, + "runtimeMonthlyUsd": runtime_monthly_usd, + "byPlan": { + plan: { + "seats": seats, + "cSeatUsd": runtime_monthly_usd / seats, + } + for plan, seats in HOSTED_TOOLS_SEATS.items() + }, + } + + +def measure_subprocess(name: str, command: list[str], timeout: float = 30) -> Measurement: + start = time.perf_counter() + try: + result = subprocess.run(command, capture_output=True, text=True, timeout=timeout, check=False) + elapsed_ms = (time.perf_counter() - start) * 1000 + return Measurement( + name=name, + ok=result.returncode == 0, + elapsed_ms=elapsed_ms, + detail={ + "returnCode": result.returncode, + "stdoutBytes": len(result.stdout or ""), + "stderrBytes": len(result.stderr or ""), + }, + ) + except Exception as exc: # pragma: no cover - exercised in integration use + elapsed_ms = (time.perf_counter() - start) * 1000 + return Measurement(name=name, ok=False, elapsed_ms=elapsed_ms, detail={"error": str(exc)}) + + +def measure_entrypoint_tool(name: str, arguments: dict[str, Any] | None = None) -> Measurement: + from scripts.entrypoint import handle_mcp_json_rpc + + body = json.dumps({ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": {"name": name, "arguments": arguments or {}}, + }).encode() + start = time.perf_counter() + status, response = handle_mcp_json_rpc(body, {}) + elapsed_ms = (time.perf_counter() - start) * 1000 + text = "" + try: + text = response["result"]["content"][0]["text"] + except Exception: + text = json.dumps(response)[:500] + return Measurement( + name=f"mcp.{name}", + ok=status == 200, + elapsed_ms=elapsed_ms, + detail={ + "httpStatus": status, + "responseBytes": len(json.dumps(response)), + "containsConfirmationRefusal": "confirmed=true" in text, + }, + ) + + +def openrouter_probe(model: str) -> dict[str, Any]: + key = os.environ.get("OPENROUTER_API_KEY") + if not key: + return {"ok": False, "blocker": "OPENROUTER_API_KEY is not set."} + payload = json.dumps({ + "model": model, + "messages": [{"role": "user", "content": "Reply with exactly: ok"}], + "max_tokens": 8, + }).encode() + req = urllib.request.Request( + "https://openrouter.ai/api/v1/chat/completions", + data=payload, + headers={ + "authorization": f"Bearer {key}", + "content-type": "application/json", + "http-referer": "https://nunchi.trade", + "x-title": "nunchi-pricing-measurement", + }, + method="POST", + ) + start = time.perf_counter() + try: + with urllib.request.urlopen(req, timeout=30) as response: + body = json.loads(response.read().decode()) + elapsed_ms = (time.perf_counter() - start) * 1000 + return { + "ok": True, + "model": model, + "elapsedMs": elapsed_ms, + "usage": body.get("usage"), + "provider": body.get("provider"), + "idPresent": bool(body.get("id")), + } + except urllib.error.HTTPError as exc: + elapsed_ms = (time.perf_counter() - start) * 1000 + return {"ok": False, "elapsedMs": elapsed_ms, "status": exc.code, "blocker": exc.reason} + except Exception as exc: # pragma: no cover - network dependent + elapsed_ms = (time.perf_counter() - start) * 1000 + return {"ok": False, "elapsedMs": elapsed_ms, "blocker": str(exc)} + + +def parse_runtime_monthly(args: argparse.Namespace) -> float | None: + raw = args.runtime_monthly_usd or os.environ.get("RAILWAY_SHARED_RUNTIME_MONTHLY_USD") + if raw in (None, ""): + return None + return float(raw) + + +def build_report(args: argparse.Namespace) -> dict[str, Any]: + runtime_monthly = parse_runtime_monthly(args) + fee_tenths_bps = builder_fee_rate_tenths_bps() + measurements = [ + measure_subprocess("python.import_cli", [sys.executable, "-c", "import cli.main; print('ok')"]), + measure_entrypoint_tool("strategies"), + measure_entrypoint_tool("funding_hedge_execute", {"coin": "BTC", "dry_run": True}), + ] + openrouter = openrouter_probe(args.openrouter_model) if args.openrouter_live else { + "ok": False, + "blocker": "Run with --openrouter-live to spend OpenRouter credits for this probe.", + "credentialPresent": env_flag("OPENROUTER_API_KEY"), + } + blockers: list[str] = [] + if runtime_monthly is None: + blockers.append("missing Railway runtime monthly cost input for Mode 1 C_seat") + if not has_wallet_credentials(): + blockers.append("missing funded-wallet/HL signing credentials for live funded-wallet measurement") + if not env_flag("OPENROUTER_API_KEY"): + blockers.append("missing OPENROUTER_API_KEY for Mode 2 inference spend measurement") + if not args.openrouter_live: + blockers.append("OpenRouter live probe not run because --openrouter-live was not set") + + return { + "schemaVersion": 1, + "generatedAtMs": int(time.time() * 1000), + "environment": { + "runMode": os.environ.get("RUN_MODE"), + "hlTestnet": os.environ.get("HL_TESTNET"), + "openrouterCredentialPresent": env_flag("OPENROUTER_API_KEY"), + "walletCredentialPresent": has_wallet_credentials(), + "builderAddressPresent": env_flag("BUILDER_ADDRESS"), + "builderFeeTenthsBps": fee_tenths_bps, + }, + "measurements": [m.__dict__ for m in measurements], + "mode1": runtime_c_seat(runtime_monthly), + "mode2": { + "inferenceBudgetsUsd": HOSTED_INFERENCE_BUDGETS, + "openrouterProbe": openrouter, + }, + "mode3": { + "builderFeeTenthsBps": fee_tenths_bps, + "builderRevenuePer100kNotionalUsd": builder_revenue_usd(100_000, fee_tenths_bps), + "builderRevenuePer1mNotionalUsd": builder_revenue_usd(1_000_000, fee_tenths_bps), + "note": "Builder revenue is formulaic until funded-wallet fills are measured.", + }, + "blockers": blockers, + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--runtime-monthly-usd", type=float, default=None) + parser.add_argument("--openrouter-live", action="store_true") + parser.add_argument("--openrouter-model", default=os.environ.get("NUNCHI_PRICING_OPENROUTER_MODEL", "openai/gpt-4.1-mini")) + parser.add_argument("--output", type=Path, default=None) + args = parser.parse_args() + report = build_report(args) + text = json.dumps(report, indent=2, sort_keys=True) + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(text + "\n") + print(text) + + +if __name__ == "__main__": + main() diff --git a/tests/test_pricing_measure.py b/tests/test_pricing_measure.py new file mode 100644 index 0000000..14ff2e2 --- /dev/null +++ b/tests/test_pricing_measure.py @@ -0,0 +1,27 @@ +from scripts import pricing_measure as pricing + + +def test_builder_revenue_uses_tenths_bps(): + assert pricing.builder_revenue_usd(100_000, 100) == 100.0 + assert pricing.builder_revenue_usd(1_000_000, 25) == 250.0 + + +def test_runtime_c_seat_requires_explicit_input(): + missing = pricing.runtime_c_seat(None) + assert missing["computed"] is False + assert "blocker" in missing + + computed = pricing.runtime_c_seat(250) + assert computed["computed"] is True + assert computed["byPlan"]["starter"]["cSeatUsd"] == 50 + assert computed["byPlan"]["growth"]["cSeatUsd"] == 25 + assert computed["byPlan"]["team"]["cSeatUsd"] == 5 + + +def test_entrypoint_refusal_measurement_is_safe(): + result = pricing.measure_entrypoint_tool( + "funding_hedge_execute", + {"coin": "BTC", "dry_run": True}, + ) + assert result.ok is True + assert result.detail["containsConfirmationRefusal"] is True From 99a1e5f355b9db4abc9e4e6ab6e6ca0a7397410f Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Wed, 1 Jul 2026 16:00:41 -0400 Subject: [PATCH 4/5] fix: allow keyless hedge dry-run previews Co-authored-by: Cursor --- cli/mcp_server.py | 2 +- scripts/entrypoint.py | 2 +- tests/test_mcp_gateway_context.py | 32 +++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/cli/mcp_server.py b/cli/mcp_server.py index b37aece..d000ade 100644 --- a/cli/mcp_server.py +++ b/cli/mcp_server.py @@ -865,7 +865,7 @@ def funding_hedge_execute( env_overrides, mainnet=mainnet, confirmed=confirmed, - require_signing=True, + require_signing=not dry_run, ) if error: return _json_error(error) diff --git a/scripts/entrypoint.py b/scripts/entrypoint.py index c40832a..99fe638 100644 --- a/scripts/entrypoint.py +++ b/scripts/entrypoint.py @@ -575,7 +575,7 @@ def call_mcp_tool(name: str, arguments: dict[str, Any], headers: Any) -> str: env_overrides, mainnet=mainnet, confirmed=confirmed, - require_signing=True, + require_signing=not dry_run, ) if error: return _json_error(error) diff --git a/tests/test_mcp_gateway_context.py b/tests/test_mcp_gateway_context.py index e256583..4281594 100644 --- a/tests/test_mcp_gateway_context.py +++ b/tests/test_mcp_gateway_context.py @@ -249,3 +249,35 @@ def fake_run_hl(*args, timeout=30, env_overrides=None): assert captured["timeout"] == 120 assert captured["env_overrides"]["NUNCHI_WEB_AUTH_PAIR_TOKEN"] == "pair-token" assert captured["env_overrides"]["NUNCHI_WEB_AUTH_ADDRESS"] == "0x" + "5" * 40 + + +def test_entrypoint_funding_hedge_execute_confirmed_dry_run_allows_keyless_preview(monkeypatch, tmp_path): + import cli.mcp_server as mcp_server + from scripts.entrypoint import handle_mcp_json_rpc + + captured = {} + + def fake_run_hl(*args, timeout=30, env_overrides=None): + captured["args"] = args + captured["timeout"] = timeout + captured["env_overrides"] = env_overrides + return "keyless hedge preview" + + monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setattr(mcp_server, "_run_hl", fake_run_hl) + + body = json.dumps({ + "jsonrpc": "2.0", + "id": 6, + "method": "tools/call", + "params": { + "name": "funding_hedge_execute", + "arguments": {"coin": "BTC", "dry_run": True, "confirmed": True}, + }, + }).encode() + + status, response = handle_mcp_json_rpc(body, {}) + + assert status == 200 + assert response["result"]["content"][0]["text"] == "keyless hedge preview" + assert captured["args"] == ("hedge", "execute", "BTC", "--dry-run", "--yes") From 92db4d63d1435960c1aa83bca78a7fef5aa35f48 Mon Sep 17 00:00:00 2001 From: JaeLeex Date: Wed, 1 Jul 2026 16:22:36 -0400 Subject: [PATCH 5/5] feat: extend MCP pricing costing harness Co-authored-by: Cursor --- docs/MCP_PRICING_MEASUREMENTS.md | 35 ++++- scripts/pricing_measure.py | 260 +++++++++++++++++++++++++++++++ tests/test_pricing_measure.py | 16 ++ 3 files changed, 306 insertions(+), 5 deletions(-) diff --git a/docs/MCP_PRICING_MEASUREMENTS.md b/docs/MCP_PRICING_MEASUREMENTS.md index 0e997f8..4c1b2a3 100644 --- a/docs/MCP_PRICING_MEASUREMENTS.md +++ b/docs/MCP_PRICING_MEASUREMENTS.md @@ -23,11 +23,25 @@ Production runner env dry-run: - MCP `funding_hedge_execute` without `confirmed=true`: 0.09 ms refusal, no order path. - Railway resource metrics, last 1h: `<0.01 vCPU`, 15 MB memory, 0 MB network, 0 MB disk. -Local dry-run: - -- `python.import_cli`: 62.4 ms. -- MCP `strategies`: 120.8 ms. -- MCP `funding_hedge_execute` without `confirmed=true`: 0.10 ms refusal. +Local Task 7 dry-run (`tmp/pricing-measurement-task7-local.json`): + +- `python.import_cli`: 69.7 ms. +- MCP `tools/list`: 0.01 ms, 24 hosted runner tools surfaced by the JSON-RPC wrapper. +- MCP `setup_check`: 7.9 ms. +- MCP `strategies`: 166.8 ms, 3,068 response bytes. +- MCP `trade` without signing context: 0.09 ms refusal. This is the safe + noninteractive confirmation/hang check; no subprocess order path was entered. +- MCP `funding_hedge_execute` without `confirmed=true`: 0.03 ms refusal. + +The pricing harness now emits the full Task 7 classification: + +- Free/read: 15 tools. +- Paid compute/inference cost centers: 5 tools. +- Safety-gated/fund-moving/wallet-write: 7 tools, or 6 if `wallet_auto` is + excluded from the costable 26-tool surface because it is disabled on the + hosted keyless runner. +- Recommended beta free cap: about 20 hosted MCP discovery/read calls before + subscription or upgrade nudges. ## Economics @@ -39,11 +53,22 @@ Mode 2, hosted MCP tools plus Nunchi/OpenRouter inference: - No live OpenRouter spend was measured because `OPENROUTER_API_KEY` is not present in the runner env and `--openrouter-live` was not run. - Current inference budgets remain inputs only: Starter `$10`, Growth `$50`, Team `$250`. +- Anchor estimates from the Task 7 prompt: + - `openai/gpt-4.1-mini` at about `$0.0002` per heartbeat gives about + 50,000 / 250,000 / 1,250,000 heartbeats for Starter / Growth / Team. + - `openrouter/auto` at about `$0.0037` per heartbeat gives about + 2,703 / 13,514 / 67,568 heartbeats. + - Fusion at about `$0.033` per capped run, provided as about 146x mini, gives + about 303 / 1,515 / 7,576 runs. Mode 3, clone/local plus builder economics: - No funded-wallet fill measurement was run because the production runner has no `HL_PRIVATE_KEY`, `HL_KEYSTORE_PASSWORD`, or `~/.hl-agent/env`. - Formulaic builder-fee economics at the default `BUILDER_FEE_TENTHS_BPS=100` are `$100` per `$100,000` notional and `$1,000` per `$1,000,000` notional. +- `trade` itself should remain free or low-friction from a pricing standpoint: + it is safety-sensitive, not inference-heavy, and it is the path that can + produce builder-code/builder-fee economics. Gate it with confirmation, + builder-code validation, network consent, size limits, and signing context. ## Blockers diff --git a/scripts/pricing_measure.py b/scripts/pricing_measure.py index 2bc7c43..de6f30c 100644 --- a/scripts/pricing_measure.py +++ b/scripts/pricing_measure.py @@ -36,6 +36,189 @@ "team": 250.0, } +FREE_TOOL_CALL_LIMIT_RECOMMENDATION = 20 + +TOOL_CLASSIFICATION = [ + { + "tool": "strategies", + "bucket": "free_read", + "marginalCost": "local registry read; no inference", + "policy": "free tier", + }, + { + "tool": "builder_status", + "bucket": "free_read", + "marginalCost": "local config read; no inference", + "policy": "free tier", + }, + { + "tool": "wallet_list", + "bucket": "free_read", + "marginalCost": "local keystore metadata read; no inference", + "policy": "free tier", + }, + { + "tool": "setup_check", + "bucket": "free_read", + "marginalCost": "local environment/config checks; no inference", + "policy": "free tier", + }, + { + "tool": "pair_status", + "bucket": "free_read", + "marginalCost": "web-auth pair/session read; no inference", + "policy": "free tier", + }, + { + "tool": "account", + "bucket": "free_read", + "marginalCost": "Hyperliquid/Hydromancer info read; no inference", + "policy": "free tier with light rate limit", + }, + { + "tool": "status", + "bucket": "free_read", + "marginalCost": "position/risk read; no inference", + "policy": "free tier with light rate limit", + }, + { + "tool": "funding_hedge_propose", + "bucket": "free_read", + "marginalCost": "deterministic hedge proposal; no LLM by default", + "policy": "free tier", + }, + { + "tool": "funding_hedge_backtest", + "bucket": "free_read", + "marginalCost": "bounded CPU/backtest work; no inference", + "policy": "free tier with abuse cap", + }, + { + "tool": "apex_status", + "bucket": "free_read", + "marginalCost": "local status read; no inference", + "policy": "free tier", + }, + { + "tool": "agent_memory", + "bucket": "free_read", + "marginalCost": "local memory file read; no inference", + "policy": "free tier", + }, + { + "tool": "trade_journal", + "bucket": "free_read", + "marginalCost": "local journal read; no inference", + "policy": "free tier", + }, + { + "tool": "judge_report", + "bucket": "free_read", + "marginalCost": "latest report read; no inference", + "policy": "free tier", + }, + { + "tool": "obsidian_context", + "bucket": "free_read", + "marginalCost": "local vault/context read; no inference", + "policy": "free tier", + }, + { + "tool": "money_bridge_status", + "bucket": "free_read", + "marginalCost": "bridge status read; no inference", + "policy": "free tier", + }, + { + "tool": "run_strategy", + "bucket": "paid_compute", + "marginalCost": "strategy loop CPU/API calls; may invoke inference depending on strategy", + "policy": "paid compute quota", + }, + { + "tool": "radar_run", + "bucket": "paid_compute", + "marginalCost": "market scan CPU/API calls; may be inference-backed in premium paths", + "policy": "paid compute quota", + }, + { + "tool": "apex_run", + "bucket": "paid_compute", + "marginalCost": "multi-slot orchestrator; highest sustained runtime/API risk", + "policy": "paid compute quota", + }, + { + "tool": "reflect_run", + "bucket": "paid_compute", + "marginalCost": "post-trade review; likely LLM-backed when reports are generated", + "policy": "paid compute quota", + }, + { + "tool": "hedge_agent_smoke_test", + "bucket": "paid_compute", + "marginalCost": "agent smoke/eval path; possibly inference-backed", + "policy": "paid compute quota", + }, + { + "tool": "trade", + "bucket": "safety_gated", + "marginalCost": "order submission; not inference-heavy and can create builder economics", + "policy": "free or low-friction, confirmation and limit gated", + }, + { + "tool": "funding_hedge_execute", + "bucket": "safety_gated", + "marginalCost": "hedge order path; fund-moving when dry_run=false", + "policy": "confirmation gated; dry-run preview safe", + }, + { + "tool": "money_withdraw", + "bucket": "safety_gated", + "marginalCost": "fund-moving transfer; no inference", + "policy": "confirmation and entitlement gated", + }, + { + "tool": "money_transfer_usd", + "bucket": "safety_gated", + "marginalCost": "fund-moving transfer; no inference", + "policy": "confirmation and entitlement gated", + }, + { + "tool": "money_deposit", + "bucket": "safety_gated", + "marginalCost": "fund-moving deposit flow; no inference", + "policy": "confirmation and entitlement gated", + }, + { + "tool": "approve_agent", + "bucket": "safety_gated", + "marginalCost": "approval/signing control; no inference", + "policy": "confirmation and policy gated", + }, + { + "tool": "wallet_auto", + "bucket": "safety_gated", + "marginalCost": "wallet creation/write; no inference", + "policy": "disabled on hosted keyless runner, gated elsewhere", + }, +] + +INFERENCE_COST_ANCHORS = { + "openrouter/auto": { + "costPerHeartbeatUsd": 0.0037, + "source": "anchor from Task 7 prompt", + }, + "openai/gpt-4.1-mini": { + "costPerHeartbeatUsd": 0.0002, + "source": "anchor from Task 7 prompt", + }, + "fusion": { + "costPerHeartbeatUsd": 0.033, + "source": "anchor from Task 7 prompt", + "providedRatioVsMini": 146, + }, +} + @dataclass(frozen=True) class Measurement: @@ -70,6 +253,31 @@ def builder_revenue_usd(notional_usd: float, fee_tenths_bps: int) -> float: return notional_usd * fee_tenths_bps / 100_000 +def tool_bucket_counts() -> dict[str, int]: + counts: dict[str, int] = {} + for row in TOOL_CLASSIFICATION: + bucket = row["bucket"] + counts[bucket] = counts.get(bucket, 0) + 1 + counts["total"] = len(TOOL_CLASSIFICATION) + counts["costedWithoutWalletAuto"] = len([row for row in TOOL_CLASSIFICATION if row["tool"] != "wallet_auto"]) + return counts + + +def inference_anchor_budget_capacity(budgets: dict[str, float]) -> dict[str, dict[str, Any]]: + return { + model: { + "costPerHeartbeatUsd": anchor["costPerHeartbeatUsd"], + "source": anchor["source"], + **({"providedRatioVsMini": anchor["providedRatioVsMini"]} if "providedRatioVsMini" in anchor else {}), + "heartbeatsByPlan": { + plan: budget / anchor["costPerHeartbeatUsd"] + for plan, budget in budgets.items() + }, + } + for model, anchor in INFERENCE_COST_ANCHORS.items() + } + + def runtime_c_seat(runtime_monthly_usd: float | None) -> dict[str, Any]: if runtime_monthly_usd is None: return { @@ -109,6 +317,31 @@ def measure_subprocess(name: str, command: list[str], timeout: float = 30) -> Me return Measurement(name=name, ok=False, elapsed_ms=elapsed_ms, detail={"error": str(exc)}) +def measure_entrypoint_method(method: str) -> Measurement: + from scripts.entrypoint import handle_mcp_json_rpc + + body = json.dumps({ + "jsonrpc": "2.0", + "id": 1, + "method": method, + }).encode() + start = time.perf_counter() + status, response = handle_mcp_json_rpc(body, {}) + elapsed_ms = (time.perf_counter() - start) * 1000 + result = response.get("result") if isinstance(response, dict) else None + tool_count = len(result.get("tools", [])) if isinstance(result, dict) and isinstance(result.get("tools"), list) else None + return Measurement( + name=f"mcp.{method}", + ok=status == 200, + elapsed_ms=elapsed_ms, + detail={ + "httpStatus": status, + "responseBytes": len(json.dumps(response)), + "toolCount": tool_count, + }, + ) + + def measure_entrypoint_tool(name: str, arguments: dict[str, Any] | None = None) -> Measurement: from scripts.entrypoint import handle_mcp_json_rpc @@ -134,10 +367,25 @@ def measure_entrypoint_tool(name: str, arguments: dict[str, Any] | None = None) "httpStatus": status, "responseBytes": len(json.dumps(response)), "containsConfirmationRefusal": "confirmed=true" in text, + "containsSigningRefusal": "requires a signing context" in text, }, ) +def safe_trade_refusal_measurement() -> Measurement: + if has_wallet_credentials(): + return Measurement( + name="mcp.trade_unsigned_refusal", + ok=True, + elapsed_ms=0, + detail={ + "skipped": True, + "reason": "wallet credentials are present; skipping trade probe to avoid any order path", + }, + ) + return measure_entrypoint_tool("trade", {"instrument": "ETH-PERP", "side": "buy", "size": 0.1}) + + def openrouter_probe(model: str) -> dict[str, Any]: key = os.environ.get("OPENROUTER_API_KEY") if not key: @@ -191,7 +439,10 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: fee_tenths_bps = builder_fee_rate_tenths_bps() measurements = [ measure_subprocess("python.import_cli", [sys.executable, "-c", "import cli.main; print('ok')"]), + measure_entrypoint_method("tools/list"), + measure_entrypoint_tool("setup_check"), measure_entrypoint_tool("strategies"), + safe_trade_refusal_measurement(), measure_entrypoint_tool("funding_hedge_execute", {"coin": "BTC", "dry_run": True}), ] openrouter = openrouter_probe(args.openrouter_model) if args.openrouter_live else { @@ -212,6 +463,14 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: return { "schemaVersion": 1, "generatedAtMs": int(time.time() * 1000), + "freeTierRecommendation": { + "hostedMcpFreeCallLimit": FREE_TOOL_CALL_LIMIT_RECOMMENDATION, + "note": "Use this as a beta/free-tier cap for hosted MCP discovery/read calls; keep paid-compute and fund-moving actions separately metered/gated.", + }, + "toolClassification": { + "counts": tool_bucket_counts(), + "tools": TOOL_CLASSIFICATION, + }, "environment": { "runMode": os.environ.get("RUN_MODE"), "hlTestnet": os.environ.get("HL_TESTNET"), @@ -224,6 +483,7 @@ def build_report(args: argparse.Namespace) -> dict[str, Any]: "mode1": runtime_c_seat(runtime_monthly), "mode2": { "inferenceBudgetsUsd": HOSTED_INFERENCE_BUDGETS, + "anchorEstimates": inference_anchor_budget_capacity(HOSTED_INFERENCE_BUDGETS), "openrouterProbe": openrouter, }, "mode3": { diff --git a/tests/test_pricing_measure.py b/tests/test_pricing_measure.py index 14ff2e2..98cdddd 100644 --- a/tests/test_pricing_measure.py +++ b/tests/test_pricing_measure.py @@ -18,6 +18,22 @@ def test_runtime_c_seat_requires_explicit_input(): assert computed["byPlan"]["team"]["cSeatUsd"] == 5 +def test_tool_classification_counts_match_task7_buckets(): + counts = pricing.tool_bucket_counts() + assert counts["free_read"] == 15 + assert counts["paid_compute"] == 5 + assert counts["safety_gated"] == 7 + assert counts["total"] == 27 + assert counts["costedWithoutWalletAuto"] == 26 + + +def test_inference_anchor_budget_capacity_uses_prompt_anchors(): + capacity = pricing.inference_anchor_budget_capacity({"starter": 10.0}) + assert capacity["openai/gpt-4.1-mini"]["heartbeatsByPlan"]["starter"] == 50_000 + assert round(capacity["openrouter/auto"]["heartbeatsByPlan"]["starter"], 2) == 2702.7 + assert capacity["fusion"]["providedRatioVsMini"] == 146 + + def test_entrypoint_refusal_measurement_is_safe(): result = pricing.measure_entrypoint_tool( "funding_hedge_execute",