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 @@
-
+
@@ -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",