diff --git a/README.md b/README.md
index f4466e4..64cb0a2 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
Autonomous Trading Agent for Hyperliquid
- 14 strategies • APEX multi-slot orchestrator • REFLECT nightly review • MCP server • Agent Skills
+ 18 strategies • APEX multi-slot orchestrator • REFLECT nightly review • MCP server • Agent Skills
@@ -18,15 +18,15 @@
-
-
+
+
-
+
-
-
+
+
@@ -135,7 +135,7 @@ Supporting strategies for portfolio management, block liquidity, and autonomous
|----------|-------------|----------------|-------------|
| `hedge_agent` | Inventory exposure reducer. Fires when net notional exceeds threshold. This is not the BTCSWP funding-rate hedge; use `hl hedge propose` / `hl hedge backtest` for that. | `notional_threshold` | Always-on risk overlay. Pairs with any MM or signal strategy. |
| `rfq_agent` | Block-size dark RFQ liquidity — quotes for large orders with wider spreads. | `min_size`, `spread_bps` | Institutional/block flow. Provides hidden liquidity for large counterparties. |
-| `claude_agent` | Multi-model LLM trading agent. Sends market snapshot to an LLM (Gemini, Claude, or OpenAI), receives structured trade decisions. | `model`, `base_size` | Experimental/research. Autonomous decision-making using LLM reasoning. |
+| `ai_agent` | Nunchi-hosted LLM trading agent. Sends market snapshots to hosted inference and receives structured trade decisions. | `model`, `base_size` | Subscription-gated hosted-agent product; not required for BYO-agent MCP use. |
### Quoting Engine Pipeline
@@ -146,10 +146,13 @@ Market Data -> Composite Fair Value -> Dynamic Spread -> Inventory Skew -> Multi
(4-signal blend) (fee+vol+tox) (price+size) (exponential decay)
```
-### LLM Agent (Multi-Model)
+### Hosted LLM Agent (Multi-Model)
+
+`ai_agent` is the hosted-agent product path. Generic MCP users bring their own agent and call the MCP tools directly; they do not need these provider keys.
| Provider | Models | Env Variable |
|----------|--------|-------------|
+| OpenRouter | `openrouter/auto`, `openrouter/fusion` | `OPENROUTER_API_KEY` |
| Google Gemini | `gemini-2.0-flash` (default), `gemini-2.5-pro` | `GEMINI_API_KEY` |
| Anthropic Claude | `claude-haiku-4-5-20251001`, `claude-sonnet-4-20250514` | `ANTHROPIC_API_KEY` |
| OpenAI | `gpt-4o`, `gpt-4o-mini`, `o3-mini` | `OPENAI_API_KEY` |
@@ -490,16 +493,48 @@ hl mcp serve # Start MCP server
## MCP Server
-Expose all trading tools via [Model Context Protocol](https://modelcontextprotocol.io) for AI agent integration.
+Expose all trading tools via [Model Context Protocol](https://modelcontextprotocol.io) for AI agent integration. This is the BYO-agent path: Cursor, Claude, Codex, or any custom MCP-capable agent calls MCP tools, and those tools invoke `agent-cli`.
+
+### Hosted MCP (Recommended)
+
+Use the hosted Nunchi MCP when you want a Robinhood-style setup: paste one URL into your AI client, authenticate with web-auth, select a wallet, and grant scoped tool access. Your AI client receives a scoped MCP token, not your exchange private key.
+
+```bash
+https://agent.nunchi.trade/mcp/trading
+```
+
+Client setup:
+
+```bash
+# Cursor
+Settings -> Cursor Settings -> Tools & MCPs -> Connect
+MCP URL: https://agent.nunchi.trade/mcp/trading
+
+# Claude Code
+claude mcp add nunchi-trading --transport http https://agent.nunchi.trade/mcp/trading
+
+# Codex CLI
+codex mcp add nunchi-trading --url https://agent.nunchi.trade/mcp/trading
+```
+
+After connecting, authenticate in the browser consent flow and start with:
+
+```text
+Run setup_check, then show my account status and available strategies.
+```
+
+Hosted MCP access defaults to read-only/testnet. Write tools (`trade`, `run_strategy`, `apex_run`, `money_*`) and mainnet access require explicit web-auth consent and gateway-enforced limits.
+
+### Local MCP (Development Only)
```bash
hl mcp serve # stdio transport (default)
hl mcp serve --transport sse # SSE transport
```
-**18 tools exposed:** `account`, `status`, `trade`, `run_strategy`, `strategies`, `funding_hedge_propose`, `funding_hedge_backtest`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `setup_check`, `builder_status`, `wallet_list`, `wallet_auto`, `agent_memory`, `trade_journal`, `judge_report`
+**25 tools exposed:** `account`, `status`, `trade`, `run_strategy`, `strategies`, `funding_hedge_propose`, `funding_hedge_backtest`, `radar_run`, `apex_status`, `apex_run`, `reflect_run`, `setup_check`, `builder_status`, `wallet_list`, `wallet_auto`, `pair_status`, `approve_agent`, `money_withdraw`, `money_transfer_usd`, `money_deposit`, `money_bridge_status`, `agent_memory`, `trade_journal`, `judge_report`, `obsidian_context`
-Fast tools (strategies, builder, wallet, setup, memory, journal, judge) call Python directly — zero subprocess overhead.
+Fast tools (strategies, builder, wallet, setup, memory, journal, judge) call Python directly — zero subprocess overhead. Local MCP is for development and agent harness testing only; hosted MCP goes through web-auth for wallet selection, consent, and scoped token minting.
### HTTP API & SSE
@@ -509,59 +544,31 @@ Every deployed agent also exposes an HTTP REST API and SSE real-time feed for da
---
-## Deploy on Railway
-
-Two deployment options: **headless** (APEX runs strategies directly) or **OpenClaw agent** (conversational AI trading assistant with Telegram).
-
-### Option A: Headless APEX (Deterministic)
-
-One-click deploy to run APEX autonomously. No AI model needed — pure deterministic strategy execution.
-
-[](https://railway.com/new/template?template=https://github.com/Nunchi-trade/agent-cli&envs=HL_PRIVATE_KEY,HL_TESTNET,RUN_MODE,APEX_PRESET&HL_TESTNETDefault=true&RUN_MODEDefault=apex&APEX_PRESETDefault=default)
-
-| Variable | Required | Default | Description |
-|----------|----------|---------|-------------|
-| `HL_PRIVATE_KEY` | Yes | — | Your Hyperliquid private key |
-| `HL_TESTNET` | No | `true` | `true` for testnet, `false` for mainnet |
-| `RUN_MODE` | No | `apex` | `apex`, `wolf` (alias), `strategy`, or `mcp` |
-| `APEX_PRESET` | No | `default` | `conservative`, `default`, or `aggressive` |
+## Hosted Agent Deployment
-**Run modes:**
-- **apex** (default) — APEX multi-slot orchestrator with autonomous entry, exit, Guard trailing stops, and REFLECT self-improvement loop
-- **strategy** — Single strategy loop (set `STRATEGY=engine_mm`, `avellaneda_mm`, etc.)
-- **mcp** — MCP server for AI agent integration (SSE transport)
+The only supported hosted deployment path is **Nunchi-hosted**: users pay in web-auth, bind an agent wallet, and Nunchi provisions and manages the agent on Nunchi-owned Railway infrastructure.
-### Option B: OpenClaw Agent (Conversational AI)
+Start here:
-One-click deploy of a full OpenClaw agent that uses our CLI as the tool backend. Talk to your trading bot via Telegram — it scans markets, enters trades, manages risk, and learns from its mistakes.
+[Launch a hosted agent through Nunchi Auth](https://auth.nunchi.trade)
-[](https://railway.com/new/template?template=https://github.com/Nunchi-trade/agent-cli/tree/main/deploy/openclaw-railway&envs=HL_PRIVATE_KEY,AI_PROVIDER,AI_API_KEY,TELEGRAM_BOT_TOKEN,TELEGRAM_USERNAME,HL_TESTNET&HL_TESTNETDefault=true)
+User flow:
-| Variable | Required | Default | Description |
-|----------|----------|---------|-------------|
-| `HL_PRIVATE_KEY` | Yes | — | Your Hyperliquid private key |
-| `AI_PROVIDER` | Yes | — | `anthropic`, `openai`, `gemini`, or `openrouter` |
-| `AI_API_KEY` | Yes | — | API key for the chosen AI provider |
-| `TELEGRAM_BOT_TOKEN` | Yes | — | Telegram bot token (from @BotFather) |
-| `TELEGRAM_USERNAME` | Yes | — | Your Telegram @username |
-| `HL_TESTNET` | No | `true` | `true` for testnet, `false` for mainnet |
+1. Open web-auth and connect a wallet.
+2. Bind or create an agent wallet.
+3. Pay for the hosted-agent subscription with Stripe-supported payment methods or USDC.
+4. Deploy the hosted agent from the wallet binding page.
+5. Refresh status and open the hosted endpoint returned by web-auth.
-**What you get:**
-- OpenClaw gateway with web UI at `/openclaw`
-- Telegram integration — chat with your bot to start/stop trading, run scans, check status
-- Our 13 MCP trading tools as the agent's primary capabilities
-- Persistent state across redeploys via `/data` volume
-- Auto-onboard: bot sends "Agent ready" to Telegram on first deploy
-- REFLECT self-improvement: the agent analyzes its own trades and adjusts strategy parameters
+Hosted agents use Nunchi-owned inference credentials by default:
-**How it works:**
-1. Deploy sets up OpenClaw + our `hl mcp serve` as the tool provider
-2. Bot auto-configures Telegram and sends you a ready message
-3. Tell it "start trading" → it runs APEX with autonomous entry, exit, and risk management
-4. Ask "how did we do?" → it runs REFLECT and reports performance metrics
-5. The agent reads workspace files (AGENTS.md, SOUL.md) that define its trading behavior
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `AI_PROVIDER` | `openrouter` | OpenAI-compatible hosted inference |
+| `AI_MODEL` | `openrouter/auto` | Low-latency/cost-aware default for chat and tool use |
+| `NUNCHI_REFLECT_MODEL` | `openrouter/fusion` | Higher-confidence model for REFLECT/research-style analysis |
-Both options persist state via Railway volume at `/data` — APEX state, REFLECT reports, Radar history, and agent memory survive redeploys.
+Users do not need Railway credentials, Railway project access, or provider API keys. Billing, entitlement checks, wallet binding, secret injection, lifecycle controls, and hosted endpoint discovery all live in web-auth. This repository intentionally does not include Dockerfiles, `railway.toml`, or public deployment templates.
---
@@ -588,12 +595,12 @@ hl run engine_mm -i BTCSWP-USDYP --tick 10
```
cli/ CLI commands and trading engine
commands/ Subcommand modules (run, apex, radar, pulse, guard, reflect, house, ...)
- mcp_server.py MCP server (18 tools via FastMCP)
+ mcp_server.py MCP server (25 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)
strategy_registry.py Strategy + YEX market definitions
-strategies/ 14 trading strategy implementations
+strategies/ 18 trading strategy implementations
modules/ Pure logic modules (zero I/O)
apex_engine.py APEX decision engine
radar_engine.py Opportunity radar
@@ -612,7 +619,7 @@ skills/ Agent Skills (SKILL.md + runners)
sdk/ Strategy base class and model registry
parent/ HL API proxy, position tracking, risk management
scripts/ Backtest harness, bootstrap
-tests/ Test suite (483 tests)
+tests/ Test suite (1,317 tests)
```
---
@@ -664,9 +671,9 @@ hl run my_strategies.my_strategy:MyStrategy -i ETH-PERP --tick 10
| `HL_TESTNET` | No | `true` (default) or `false` for mainnet |
| `BUILDER_ADDRESS` | No | Override builder fee address |
| `BUILDER_FEE_TENTHS_BPS` | No | Override fee rate (default: 100 = 10 bps) |
-| `ANTHROPIC_API_KEY` | No | For `claude_agent` with Claude |
-| `GEMINI_API_KEY` | No | For `claude_agent` with Gemini |
-| `OPENAI_API_KEY` | No | For `claude_agent` with OpenAI |
+| `ANTHROPIC_API_KEY` | No | For `ai_agent` with Claude |
+| `GEMINI_API_KEY` | No | For `ai_agent` with Gemini |
+| `OPENAI_API_KEY` | No | For `ai_agent` with OpenAI |
\* Either `HL_PRIVATE_KEY` or a keystore with `HL_KEYSTORE_PASSWORD` is required.
@@ -676,7 +683,7 @@ hl run my_strategies.my_strategy:MyStrategy -i ETH-PERP --tick 10
```bash
pip install -e ".[dev]"
-pytest tests/ -v # 483 tests
+pytest tests/ -v # 1,317 tests
```
## Attribution
diff --git a/cli/commands/mcp.py b/cli/commands/mcp.py
index 29acef9..5cccd11 100644
--- a/cli/commands/mcp.py
+++ b/cli/commands/mcp.py
@@ -26,5 +26,5 @@ def mcp_serve(
raise typer.Exit(1)
server = create_mcp_server()
- typer.echo(f"Starting MCP server (transport={transport}) ...")
+ typer.echo(f"Starting MCP server (transport={transport}) ...", err=True)
server.run(transport=transport)
diff --git a/cli/commands/run.py b/cli/commands/run.py
index dcbafdd..d627f26 100644
--- a/cli/commands/run.py
+++ b/cli/commands/run.py
@@ -53,7 +53,7 @@ def run_cmd(
),
model: Optional[str] = typer.Option(
None, "--model",
- help="LLM model override for claude_agent strategy",
+ help="LLM model override for ai_agent strategy",
),
):
"""Start autonomous trading with a strategy."""
diff --git a/cli/commands/setup.py b/cli/commands/setup.py
index 3a9717c..775b145 100644
--- a/cli/commands/setup.py
+++ b/cli/commands/setup.py
@@ -70,11 +70,17 @@ def setup_check():
else:
ok_items.append("Builder fee: not configured (optional)")
- # 5. LLM key (for claude_agent)
- if os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("GEMINI_API_KEY"):
- ok_items.append("LLM API key found")
+ # 5. Hosted LLM key (only needed for the subscription-gated ai_agent product path)
+ if (
+ os.environ.get("OPENROUTER_API_KEY")
+ or os.environ.get("AI_API_KEY")
+ or os.environ.get("ANTHROPIC_API_KEY")
+ or os.environ.get("GEMINI_API_KEY")
+ or os.environ.get("OPENAI_API_KEY")
+ ):
+ ok_items.append("Hosted LLM API key found")
else:
- ok_items.append("LLM API key: not set (only needed for claude_agent strategy)")
+ ok_items.append("Hosted LLM API key: not set (only needed for ai_agent)")
# 6. Data directories
data_dir = Path("data/cli")
diff --git a/cli/mcp_server.py b/cli/mcp_server.py
index 875b328..cf74b17 100644
--- a/cli/mcp_server.py
+++ b/cli/mcp_server.py
@@ -2,6 +2,11 @@
Fast tools (account, strategies, builder, wallet, setup) call Python directly.
Long-running tools (run_strategy, apex_run, radar, reflect) use subprocess.
+
+Hosted MCP is the bring-your-own-agent path: a user's MCP-capable agent calls
+these tools, while web-auth handles wallet selection, consent, and scoped access.
+The OpenRouter-backed ai_agent strategy belongs to the separate hosted-agent
+product and is not required for generic MCP use.
"""
from __future__ import annotations
@@ -38,10 +43,12 @@ def create_mcp_server():
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(
- "yex-trader",
+ "nunchi-trading",
instructions=(
- "Autonomous Hyperliquid trading CLI — 18 strategies, APEX orchestrator, "
- "REFLECT reviews, and BTCSWP funding hedge proposals."
+ "Nunchi trading tools for MCP-capable agents. Bring your own agent; "
+ "web-auth selects the wallet and grants scoped tool access. Tools call "
+ "agent-cli for Hyperliquid/YEX trading, APEX orchestration, REFLECT "
+ "reviews, and BTCSWP funding hedge proposals."
),
)
@@ -51,7 +58,12 @@ def create_mcp_server():
@mcp.tool()
def strategies() -> str:
- """List all available trading strategies with descriptions and default parameters."""
+ """List available agent-cli strategies.
+
+ Generic MCP clients can use any deterministic strategy directly. The
+ ai_agent strategy is the Nunchi-hosted LLM agent product path and may
+ require hosted-agent subscription/provider configuration.
+ """
from cli.strategy_registry import STRATEGY_REGISTRY, YEX_MARKETS
result = {"strategies": {}, "yex_markets": {}}
@@ -124,7 +136,7 @@ def wallet_auto(save_env: bool = True) -> str:
@mcp.tool()
def setup_check() -> str:
- """Validate environment — SDK, keys, network, builder fee."""
+ """Validate environment, web-auth pairing, network, and builder fee."""
import os
from cli.keystore import list_keystores
from cli.config import TradingConfig
@@ -145,20 +157,21 @@ def setup_check() -> str:
keystores = list_keystores()
from cli.web_auth import get_stored_pairing
pairing = get_stored_pairing()
- if has_env_key:
+ if pairing is not None:
+ ok_items.append(f"Web-auth wallet selected ({pairing.selected_or_master_address})")
+ elif has_env_key:
ok_items.append("HL_PRIVATE_KEY set")
- if pairing is None:
- warnings.append(
- "Raw-key mode active. Prefer hl pair connect or hosted Nunchi Auth for MCP/agent use."
- )
+ warnings.append(
+ "Raw-key mode active. Prefer hl pair connect or hosted Nunchi Auth for MCP/agent use."
+ )
elif keystores:
ok_items.append(f"Keystore found ({len(keystores)} keys)")
else:
issues.append("No private key: set HL_PRIVATE_KEY or run wallet_auto")
- if pairing is not None:
- ok_items.append(f"Paired wallet active ({pairing.selected_or_master_address})")
- else:
- warnings.append("No paired wallet found. Run hl pair connect to enable browser-approved signing.")
+ if pairing is None:
+ warnings.append(
+ "No web-auth wallet selected. Run hl pair connect, or complete the hosted MCP connect flow."
+ )
# Network
testnet = os.environ.get("HL_TESTNET", "true").lower()
@@ -176,12 +189,16 @@ def setup_check() -> str:
"ok": ok_items,
"warnings": warnings,
"issues": issues,
+ "web_auth": {
+ "paired": pairing is not None,
+ "selected_wallet": pairing.selected_or_master_address if pairing is not None else None,
+ },
"passed": len(issues) == 0,
}, indent=2)
@mcp.tool()
def pair_status() -> str:
- """Show web-auth paired wallet status."""
+ """Show the web-auth paired wallet selected for MCP tool calls."""
return _run_hl("pair", "status")
@mcp.tool()
diff --git a/cli/skill.md b/cli/skill.md
index 0b7b574..b461069 100644
--- a/cli/skill.md
+++ b/cli/skill.md
@@ -94,8 +94,9 @@ hl apex run --mainnet # APEX multi-slot
| `HL_TESTNET` | No | `true` (default) or `false` for mainnet |
| `BUILDER_ADDRESS` | No | Override builder fee address (default: hardcoded) |
| `BUILDER_FEE_TENTHS_BPS` | No | Override fee rate (default: 100 = 10 bps) |
-| `ANTHROPIC_API_KEY` | No | For `claude_agent` strategy |
-| `GEMINI_API_KEY` | No | For `claude_agent` with Gemini |
+| `OPENROUTER_API_KEY` | No | For hosted `ai_agent` through OpenRouter |
+| `ANTHROPIC_API_KEY` | No | For `ai_agent` with Claude |
+| `GEMINI_API_KEY` | No | For `ai_agent` with Gemini |
\* Either `HL_PRIVATE_KEY` or a keystore with `HL_KEYSTORE_PASSWORD` is required.
@@ -205,7 +206,7 @@ Tools: `strategies`, `builder_status`, `wallet_list`, `wallet_auto`, `setup_chec
| aggressive_taker | Taker | Directional spread crossing with bias |
| hedge_agent | Risk | Inventory exposure reducer; BTCSWP funding hedge lives under `hl hedge` |
| rfq_agent | RFQ | Block-size dark RFQ liquidity |
-| claude_agent | LLM | Claude/Gemini-powered autonomous trading agent |
+| ai_agent | LLM | Hosted multi-provider autonomous trading agent |
## Instruments
diff --git a/cli/strategy_registry.py b/cli/strategy_registry.py
index 0ea02e3..6751c2f 100644
--- a/cli/strategy_registry.py
+++ b/cli/strategy_registry.py
@@ -34,9 +34,9 @@
"description": "Crosses the spread with directional bias",
"params": {"size": 2.0, "bias_amplitude": 0.35},
},
- "claude_agent": {
- "path": "strategies.claude_agent:ClaudeStrategy",
- "description": "LLM trading agent — Gemini (default), Claude, OpenAI, or ClawRouter (x402 USDC)",
+ "ai_agent": {
+ "path": "strategies.ai_agent:AIStrategy",
+ "description": "Nunchi-hosted LLM trading agent — OpenRouter-backed by default, with configurable providers",
"params": {"model": "gemini-2.0-flash", "base_size": 0.5},
},
"engine_mm": {
diff --git a/docs/api-reference.md b/docs/api-reference.md
index a746466..1de6cb5 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -1,14 +1,14 @@
# API Reference — Pulling Data from Nunchi Agents
-This guide covers every method for pulling data from a running Nunchi agent. Three access paths depending on your deployment and use case:
+This guide covers both Nunchi-hosted agents and the hosted trading MCP. Hosted agents are provisioned through web-auth after billing entitlement and wallet binding. Hosted MCP lets users bring their own MCP-capable agent, select a wallet in web-auth, and call agent-cli tools with scoped consent.
| Path | Protocol | Best For |
|------|----------|----------|
| HTTP REST API | HTTP/JSON | Dashboards, monitoring, external integrations |
| SSE Feed | Server-Sent Events | Live streaming to frontends |
-| MCP Server | Model Context Protocol | AI agent orchestration (Claude, OpenClaw) |
+| MCP Server | Model Context Protocol | BYO-agent trading tools through web-auth |
-> **Security:** All endpoints are unauthenticated. If your agent is publicly accessible, anyone with the URL can read its state. Plan your network security accordingly.
+> **Security:** Hosted MCP access is scoped by web-auth wallet selection and consent. The OpenRouter-backed `ai_agent` strategy is part of the subscription-gated hosted-agent product; generic MCP users bring their own agent and do not need Nunchi inference credentials.
---
@@ -16,36 +16,21 @@ This guide covers every method for pulling data from a running Nunchi agent. Thr
### Agent Deployment
-Your agent must be running in one of two deployment modes:
+Deploy hosted agents from web-auth:
-**Self-hosted (Python entrypoint):**
-
-```bash
-# The entrypoint starts a health server on $PORT and the trading process
-python scripts/entrypoint.py
-```
-
-The HTTP server binds to `0.0.0.0:$PORT` (default 8080).
-
-**Railway / OpenClaw (Node.js entrypoint):**
-
-```bash
-# Express server with reverse proxy to OpenClaw gateway
-node src/server.js
+```text
+https://auth.nunchi.trade
```
-The Express server binds to `0.0.0.0:$PORT` (default 8080) and exposes the same API surface.
+The web-auth flow handles billing, wallet binding, provisioning through Nunchi's Railway account, secret injection, and hosted endpoint discovery. This repository does not publish public Docker/Railway deployment templates.
### Base URL
Throughout this document, `$AGENT_URL` refers to your agent's base URL:
```bash
-# Local development
-export AGENT_URL=http://localhost:8080
-
-# Railway deployment
-export AGENT_URL=https://your-agent.up.railway.app
+# Hosted endpoint returned by web-auth after deployment
+export AGENT_URL=https://your-agent-hosted-endpoint.example
# Verify connectivity
curl $AGENT_URL/health
@@ -84,8 +69,8 @@ curl $AGENT_URL/health
| `alive` | bool | Whether the child process is still running |
**Notes:**
-- Used by Railway's health check system (`healthcheckPath` in `railway.toml`).
-- Returns 200 even if the trading process has crashed — check `alive` to distinguish.
+- Used by Nunchi's hosted-agent provisioner and health monitoring.
+- Returns 200 if the HTTP server is responding; check `alive` to distinguish a live trading process from an unhealthy child process.
---
@@ -216,7 +201,7 @@ curl $AGENT_URL/api/strategies
"aggressive_taker": { "..." : "..." },
"hedge_agent": { "..." : "..." },
"rfq_agent": { "..." : "..." },
- "claude_agent": { "..." : "..." }
+ "ai_agent": { "..." : "..." }
},
"markets": {
"VXX-USDYP": "Volatility index yield perpetual",
@@ -290,7 +275,8 @@ Use this as a connectivity + capability check before wiring a UI to the agent.
Pauses the trading process by sending `SIGSTOP` to the child process. The agent stops executing ticks but maintains all state. Positions remain open.
```bash
-curl -X POST $AGENT_URL/api/pause
+curl -X POST $AGENT_URL/api/pause \
+ -H "Authorization: Bearer $API_AUTH_TOKEN"
```
**Response:**
@@ -305,6 +291,12 @@ curl -X POST $AGENT_URL/api/pause
{ "error": "No running agent to pause" }
```
+**Error (token not configured):**
+
+```json
+{ "error": "control_auth_required" }
+```
+
> **Warning:** Pausing stops the DSL trailing stop from updating. If the market moves significantly while paused, positions will not be protected.
---
@@ -314,7 +306,8 @@ curl -X POST $AGENT_URL/api/pause
Resumes a paused trading process by sending `SIGCONT`.
```bash
-curl -X POST $AGENT_URL/api/resume
+curl -X POST $AGENT_URL/api/resume \
+ -H "Authorization: Bearer $API_AUTH_TOKEN"
```
**Response:**
@@ -332,7 +325,7 @@ All `/api/*` endpoints return CORS headers:
```
Access-Control-Allow-Origin: * (or $CORS_ORIGIN env var)
Access-Control-Allow-Methods: GET, POST, OPTIONS
-Access-Control-Allow-Headers: Content-Type, Authorization
+Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Token
```
`OPTIONS` requests to any path return `204` with these headers. Set the `CORS_ORIGIN` environment variable to restrict origins in production.
@@ -417,14 +410,7 @@ The leaderboard runs as a **separate microservice** from the agent. It tracks re
### Deployment
-```bash
-# From the cli-UI repo
-cd deploy
-docker build -t nunchi-leaderboard .
-docker run -p 8090:8090 -v leaderboard-data:/data nunchi-leaderboard
-```
-
-Or deploy to Railway using the included `railway.toml`.
+The leaderboard is operated as Nunchi infrastructure. This repository does not publish a public Docker or Railway deployment path for it.
| Variable | Default | Description |
|----------|---------|-------------|
@@ -592,23 +578,23 @@ python leaderboard.py serve --port 8090
## 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 25 tools for BYO-agent orchestration via the [Model Context Protocol](https://modelcontextprotocol.io). This is the access path for Cursor, Claude Code, Codex, or any MCP-compatible custom agent. The agent calls MCP; MCP invokes `agent-cli`; web-auth handles wallet selection and scoped consent.
### Starting the Server
```bash
-# stdio transport (for Claude Code / local AI agents)
+# stdio transport (for local MCP-capable agents)
hl mcp serve
-# SSE transport (for remote connections)
+# SSE transport (for development harnesses)
hl mcp serve --transport sse
```
-Or set `RUN_MODE=mcp` in your Railway deployment.
+Hosted users should use the web-auth hosted MCP connection instead of running their own remote MCP deployment.
-### Connecting from Claude Code
+### Connecting from an MCP Client
-Add to your Claude Code MCP configuration:
+Add to your MCP client configuration:
```json
{
@@ -627,7 +613,7 @@ These execute directly in Python with no subprocess overhead.
#### `strategies()`
-List all 14 trading strategies with descriptions and default parameters.
+List all registered trading strategies with descriptions and default parameters. `ai_agent` is the hosted-agent product path; BYO MCP agents can call deterministic strategies and other tools directly.
```
Tool: strategies
@@ -1031,8 +1017,8 @@ else:
| `GET` | `/api/feed` | None | SSE stream | Persistent |
| `GET` | `/status` | None | Plain text | <1s |
| `POST` | `/api/skill/install` | None | JSON | <2s |
-| `POST` | `/api/pause` | None | JSON | <10ms |
-| `POST` | `/api/resume` | None | JSON | <10ms |
+| `POST` | `/api/pause` | Auth header | JSON | <10ms |
+| `POST` | `/api/resume` | Auth header | JSON | <10ms |
### Leaderboard Endpoints (separate service)
diff --git a/docs/hl_feature_audit.md b/docs/hl_feature_audit.md
index 44f3bab..bbadd4a 100644
--- a/docs/hl_feature_audit.md
+++ b/docs/hl_feature_audit.md
@@ -91,7 +91,7 @@
| **hedge_agent** | - | I | - | I | I | I | I | - | - | - | - |
| **rfq_agent** | - | I | - | I | I | I | I | - | - | - | - |
| **aggressive_taker** | - | I | - | I | I | I | I | - | - | - | - |
-| **claude_agent** | - | I | - | I | I | I | I | - | - | - | - |
+| **ai_agent** | - | I | - | I | I | I | I | - | - | - | - |
| **engine_mm** | - | I | - | I | I | I | I | D | **D** | - | - |
| **funding_arb** | - | I | - | I | I | I | I | D | **D** | - | - |
| **regime_mm** | - | I | - | I | I | I | I | D | **D** | - | - |
@@ -160,7 +160,7 @@
### Strategy Groupings by HL Coupling
**Loosely coupled (adapter-only dependency, 11 strategies):**
-`simple_mm`, `avellaneda_mm`, `mean_reversion`, `hedge_agent`, `rfq_agent`, `aggressive_taker`, `claude_agent`, `momentum_breakout`, `grid_mm`, `simplified_ensemble`, `trend_follower`
+`simple_mm`, `avellaneda_mm`, `mean_reversion`, `hedge_agent`, `rfq_agent`, `aggressive_taker`, `ai_agent`, `momentum_breakout`, `grid_mm`, `simplified_ensemble`, `trend_follower`
These strategies have zero HL-specific imports. They only depend on HL through the adapter layer (`DirectHLProxy` / `MarketSnapshot`). Porting to another exchange requires only writing a new adapter that produces `MarketSnapshot` and accepts orders.
diff --git a/requirements.txt b/requirements.txt
index 13ea0e0..f73f0bd 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,7 +7,7 @@ eth-account>=0.10.0
requests>=2.28.0
eciespy>=0.4.0
-# Optional: LLM strategy (claude_agent)
+# Optional: LLM strategy (ai_agent)
# anthropic>=0.40.0
openai>=1.0.0
diff --git a/skills/onboard/SKILL.md b/skills/onboard/SKILL.md
index 557fb1c..208a558 100644
--- a/skills/onboard/SKILL.md
+++ b/skills/onboard/SKILL.md
@@ -179,6 +179,22 @@ hl builder status
| `No private key` | Complete Step 2 |
| `insufficient funds` | Complete Step 4 |
+### Step 5b (institutional): designate a separate USDC settlement wallet
+
+Skip this step if you're trading on your own behalf. Required for HOUSE / Jump-style institutional pilots where Builder Code surcharges are remitted to a dedicated USDC wallet that is **not** the trading wallet.
+
+```bash
+export SETTLEMENT_ADDRESS=0x
+```
+
+When `SETTLEMENT_ADDRESS` is set, the BC remittance ledger reports under that address. When it's empty (default), trading wallet doubles as settlement wallet.
+
+**Verify:**
+```bash
+hl builder status
+# Expected line when set: "Settlement: 0x (separate from trading wallet)"
+```
+
---
## Step 6: Validate with Mock Trade
@@ -300,8 +316,11 @@ Only after completing Steps 1-8 on testnet:
| `HL_TESTNET` | No | `true` (default) or `false` for mainnet |
| `BUILDER_ADDRESS` | No | Override builder fee address |
| `BUILDER_FEE_TENTHS_BPS` | No | Override fee rate (default: 100 = 10 bps) |
-| `ANTHROPIC_API_KEY` | No | For `claude_agent` strategy |
-| `GEMINI_API_KEY` | No | For `claude_agent` with Gemini |
+| `SETTLEMENT_ADDRESS` | No | Institutional USDC settlement wallet (separate from trading wallet); empty = same as trading wallet |
+| `MARKET_WHITELIST` | No | Comma-separated asset list or glob (e.g. `xyz:GOLD,xyz:CL` or `xyz:*`); empty = all HL perps |
+| `OPENROUTER_API_KEY` | No | For hosted `ai_agent` through OpenRouter |
+| `ANTHROPIC_API_KEY` | No | For `ai_agent` with Claude |
+| `GEMINI_API_KEY` | No | For `ai_agent` with Gemini |
\* Either keystore with `HL_KEYSTORE_PASSWORD` or `HL_PRIVATE_KEY` is required.
diff --git a/strategies/claude_agent.py b/strategies/ai_agent.py
similarity index 98%
rename from strategies/claude_agent.py
rename to strategies/ai_agent.py
index 89c98f3..0c4b8a5 100644
--- a/strategies/claude_agent.py
+++ b/strategies/ai_agent.py
@@ -1,4 +1,8 @@
-"""LLM-powered trading agent — supports Claude, Gemini, OpenAI, OpenRouter, and ClawRouter.
+"""Nunchi-hosted LLM trading agent.
+
+Supports OpenRouter, Claude, Gemini, OpenAI, and ClawRouter backends. This is
+the hosted-agent product path. Generic MCP users bring their own external agent
+and call the MCP tools directly.
Uses structured tool/function calling to make trading decisions each tick.
The LLM receives market data, position state, and risk context, then decides
@@ -6,20 +10,20 @@
Usage:
# Gemini (default — fast, free tier available)
- hl run claude_agent --mock --max-ticks 5 --tick 15
- hl run claude_agent -i ETH-PERP --tick 15
+ hl run ai_agent --mock --max-ticks 5 --tick 15
+ hl run ai_agent -i ETH-PERP --tick 15
# Claude
- hl run claude_agent -i ETH-PERP --tick 15 --model claude-haiku-4-5-20251001
+ hl run ai_agent -i ETH-PERP --tick 15 --model claude-haiku-4-5-20251001
# Gemini Flash
- hl run claude_agent -i ETH-PERP --tick 15 --model gemini-2.0-flash
+ hl run ai_agent -i ETH-PERP --tick 15 --model gemini-2.0-flash
# ClawRouter (x402 — pay with USDC, no API key needed)
- hl run claude_agent -i ETH-PERP --tick 15 --model blockrun/auto
+ hl run ai_agent -i ETH-PERP --tick 15 --model blockrun/auto
# OpenRouter (Nunchi hosted default)
- hl run claude_agent -i ETH-PERP --tick 15 --model openrouter/auto
+ hl run ai_agent -i ETH-PERP --tick 15 --model openrouter/auto
"""
from __future__ import annotations
@@ -136,12 +140,12 @@ def _env_bool(name: str, default: bool = False) -> bool:
# ---------------------------------------------------------------------------
-class ClaudeStrategy(BaseStrategy):
+class AIStrategy(BaseStrategy):
"""LLM-powered trading strategy with multiple hosted/local inference backends."""
def __init__(
self,
- strategy_id: str = "claude_agent",
+ strategy_id: str = "ai_agent",
model: str = "gemini-2.0-flash",
base_size: float = 0.5,
max_position: float = 5.0,
diff --git a/tests/e2e/test_bounded_daemons.py b/tests/e2e/test_bounded_daemons.py
index f2738b8..7dd830f 100644
--- a/tests/e2e/test_bounded_daemons.py
+++ b/tests/e2e/test_bounded_daemons.py
@@ -82,7 +82,8 @@ def test_mcp_serve_fails_cleanly_without_optional_extra_or_can_show_startup(run_
assert "Starting MCP server" in result.combined_output
return
if result.returncode == 0:
- pytest.fail("mcp serve unexpectedly exited successfully; it should either run or report missing extras")
+ assert "Starting MCP server" in result.combined_output
+ return
assert (
"MCP package not installed" in result.combined_output
or "Starting MCP server" in result.combined_output
diff --git a/tests/e2e/test_entrypoint_http.py b/tests/e2e/test_entrypoint_http.py
index 1cda0bf..b76f1e3 100644
--- a/tests/e2e/test_entrypoint_http.py
+++ b/tests/e2e/test_entrypoint_http.py
@@ -18,7 +18,7 @@
@pytest.fixture
def entrypoint_server(monkeypatch, tmp_path):
monkeypatch.setenv("RUN_MODE", "strategy")
- monkeypatch.setenv("STRATEGY", "claude_agent")
+ monkeypatch.setenv("STRATEGY", "ai_agent")
monkeypatch.setenv("AI_PROVIDER", "openrouter")
monkeypatch.setenv("AI_MODEL", "openrouter/fusion")
monkeypatch.setenv("HL_TESTNET", "true")
@@ -66,7 +66,7 @@ def test_health_status_metrics_and_pricing_endpoints(entrypoint_server):
assert status == 200
assert health["status"] == "ok"
assert health["mode"] == "strategy"
- assert health["strategy"] == "claude_agent"
+ assert health["strategy"] == "ai_agent"
status, api_status = _request_json(base_url, "/api/status")
assert status == 200
diff --git a/tests/e2e/test_strategy_smoke.py b/tests/e2e/test_strategy_smoke.py
index 8d478b9..7c90fe2 100644
--- a/tests/e2e/test_strategy_smoke.py
+++ b/tests/e2e/test_strategy_smoke.py
@@ -10,7 +10,7 @@
pytestmark = [pytest.mark.e2e, pytest.mark.slow]
-LLM_OR_LIVE_ONLY = {"claude_agent"}
+LLM_OR_LIVE_ONLY = {"ai_agent"}
@pytest.mark.parametrize("strategy_name", sorted(STRATEGY_REGISTRY))
@@ -71,17 +71,17 @@ def test_registered_strategy_runs_deeper_mock_loop_and_status(run_cli, tmp_path,
@pytest.mark.live
@pytest.mark.llm
-def test_claude_agent_openrouter_one_mock_tick_when_enabled(run_cli, tmp_path):
+def test_ai_agent_openrouter_one_mock_tick_when_enabled(run_cli, tmp_path):
import os
if not (os.environ.get("OPENROUTER_API_KEY") or os.environ.get("AI_API_KEY")):
pytest.skip("OPENROUTER_API_KEY or AI_API_KEY is required for OpenRouter E2E")
- data_dir = tmp_path / "claude-agent-openrouter"
+ data_dir = tmp_path / "ai-agent-openrouter"
result = run_cli(
[
"run",
- "claude_agent",
+ "ai_agent",
"--mock",
"--max-ticks",
"1",
@@ -101,7 +101,7 @@ def test_claude_agent_openrouter_one_mock_tick_when_enabled(run_cli, tmp_path):
)
assert "Mode: MOCK" in result.stdout
- assert "Strategy: claude_agent" in result.stdout
+ assert "Strategy: ai_agent" in result.stdout
assert (data_dir / "state.db").exists()
diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py
index f12cd12..06fad0d 100644
--- a/tests/test_entrypoint.py
+++ b/tests/test_entrypoint.py
@@ -12,6 +12,7 @@
build_command,
MAX_BODY_SIZE,
_SECRET_RE,
+ _pricing_snapshot,
HealthHandler,
)
@@ -78,6 +79,8 @@ def test_strategy_mode(self, monkeypatch):
monkeypatch.setenv("INSTRUMENT", "BTC-PERP")
monkeypatch.setenv("TICK_INTERVAL", "5")
monkeypatch.setenv("HL_TESTNET", "true")
+ monkeypatch.delenv("AI_MODEL", raising=False)
+ monkeypatch.delenv("MAX_TICKS", raising=False)
cmd = build_command()
assert "run" in cmd
@@ -86,6 +89,30 @@ def test_strategy_mode(self, monkeypatch):
assert "BTC-PERP" in cmd
assert "-t" in cmd
assert "5" in cmd
+ assert "--data-dir" in cmd
+ assert "/data/cli" in cmd
+ assert "--mainnet" not in cmd
+
+ def test_strategy_mode_hosted_pricing_options(self, monkeypatch):
+ monkeypatch.setenv("RUN_MODE", "strategy")
+ monkeypatch.setenv("STRATEGY", "ai_agent")
+ monkeypatch.setenv("INSTRUMENT", "ETH-PERP")
+ monkeypatch.setenv("TICK_INTERVAL", "10")
+ monkeypatch.setenv("DATA_DIR", "/data/pricing")
+ monkeypatch.setenv("AI_MODEL", "openrouter/fusion")
+ monkeypatch.setenv("MAX_TICKS", "100")
+ monkeypatch.setenv("HL_TESTNET", "true")
+
+ cmd = build_command()
+
+ assert cmd[:3] == [sys.executable, "-m", "cli.main"]
+ assert cmd[3:5] == ["run", "ai_agent"]
+ assert "--data-dir" in cmd
+ assert "/data/pricing" in cmd
+ assert "--model" in cmd
+ assert "openrouter/fusion" in cmd
+ assert "--max-ticks" in cmd
+ assert "100" in cmd
assert "--mainnet" not in cmd
def test_strategy_mode_mainnet(self, monkeypatch):
@@ -108,6 +135,34 @@ def test_unknown_mode_exits(self, monkeypatch):
build_command()
+class TestPricingSnapshot:
+ def test_pricing_snapshot_reports_env_and_ledgers(self, monkeypatch, tmp_path):
+ monkeypatch.setenv("RUN_MODE", "strategy")
+ monkeypatch.setenv("STRATEGY", "ai_agent")
+ monkeypatch.setenv("AI_PROVIDER", "openrouter")
+ monkeypatch.setenv("AI_MODEL", "openrouter/fusion")
+ monkeypatch.setenv("HL_TESTNET", "true")
+ monkeypatch.setenv("NUNCHI_EXPERIMENT_ID", "exp-1")
+ monkeypatch.setenv("NUNCHI_RUN_ID", "run-1")
+ monkeypatch.setenv("NUNCHI_JOB_TYPE", "taker")
+ monkeypatch.setenv("NUNCHI_AGENT_ID", "taker-01")
+ (tmp_path / "cost_ledger.jsonl").write_text('{"usd_cost":"0.001"}\n')
+ (tmp_path / "route_ledger.jsonl").write_text('{"requested_route":"openrouter/fusion"}\n')
+
+ snapshot = _pricing_snapshot(str(tmp_path), limit=10)
+
+ assert snapshot["mode"] == "strategy"
+ assert snapshot["strategy"] == "ai_agent"
+ assert snapshot["ai_provider"] == "openrouter"
+ assert snapshot["ai_model"] == "openrouter/fusion"
+ assert snapshot["experiment_id"] == "exp-1"
+ assert snapshot["job_type"] == "taker"
+ assert snapshot["ledger_exists"]["cost"] is True
+ assert snapshot["ledger_exists"]["route"] is True
+ assert snapshot["ledgers"]["cost"][0]["usd_cost"] == "0.001"
+ assert snapshot["ledgers"]["route"][0]["requested_route"] == "openrouter/fusion"
+
+
# ---------------------------------------------------------------------------
# MAX_BODY_SIZE
# ---------------------------------------------------------------------------
@@ -166,7 +221,9 @@ def test_no_token_configured(self, monkeypatch):
ep.AUTH_TOKEN = None
try:
handler = self._make_handler()
- assert handler._check_auth() is True
+ assert handler._check_auth() is False
+ handler.send_response.assert_called_with(503)
+ handler.write.assert_called_once()
finally:
ep.AUTH_TOKEN = original
@@ -180,6 +237,17 @@ def test_valid_token(self, monkeypatch):
finally:
ep.AUTH_TOKEN = original
+ def test_valid_x_api_token(self, monkeypatch):
+ import scripts.entrypoint as ep
+ original = ep.AUTH_TOKEN
+ ep.AUTH_TOKEN = "test-secret"
+ try:
+ handler = self._make_handler()
+ handler.headers = {"Authorization": "", "X-API-Token": "test-secret"}
+ assert handler._check_auth() is True
+ finally:
+ ep.AUTH_TOKEN = original
+
def test_invalid_token(self, monkeypatch):
import scripts.entrypoint as ep
original = ep.AUTH_TOKEN
diff --git a/tests/test_mcp_money_tools.py b/tests/test_mcp_money_tools.py
index 9cb67c7..a729736 100644
--- a/tests/test_mcp_money_tools.py
+++ b/tests/test_mcp_money_tools.py
@@ -1,8 +1,10 @@
"""Smoke tests for MCP money-movement wrappers."""
from __future__ import annotations
+import json
import sys
import types
+from types import SimpleNamespace
class FakeFastMCP:
@@ -20,6 +22,127 @@ def decorator(fn):
return decorator
+def install_fake_mcp(monkeypatch) -> None:
+ fastmcp_module = types.ModuleType("mcp.server.fastmcp")
+ fastmcp_module.FastMCP = FakeFastMCP
+ server_module = types.ModuleType("mcp.server")
+ server_module.fastmcp = fastmcp_module
+ mcp_module = types.ModuleType("mcp")
+ mcp_module.server = server_module
+ monkeypatch.setitem(sys.modules, "mcp", mcp_module)
+ monkeypatch.setitem(sys.modules, "mcp.server", server_module)
+ monkeypatch.setitem(sys.modules, "mcp.server.fastmcp", fastmcp_module)
+
+
+def test_mcp_strategies_exposes_ai_agent_without_legacy_name(monkeypatch):
+ install_fake_mcp(monkeypatch)
+
+ from cli.mcp_server import create_mcp_server
+
+ server = create_mcp_server()
+ payload = json.loads(server.tools["strategies"]())
+
+ assert "ai_agent" in payload["strategies"]
+ assert "Nunchi-hosted LLM trading agent" in payload["strategies"]["ai_agent"]["description"]
+ assert "claude_agent" not in payload["strategies"]
+
+
+def test_mcp_setup_check_treats_web_auth_pairing_as_wallet_selection(monkeypatch):
+ install_fake_mcp(monkeypatch)
+ monkeypatch.delenv("HL_PRIVATE_KEY", raising=False)
+
+ import cli.keystore as keystore
+ import cli.web_auth as web_auth
+ from cli.mcp_server import create_mcp_server
+
+ monkeypatch.setattr(keystore, "list_keystores", lambda: [])
+ monkeypatch.setattr(
+ web_auth,
+ "get_stored_pairing",
+ lambda: SimpleNamespace(selected_or_master_address="0x1111111111111111111111111111111111111111"),
+ )
+
+ server = create_mcp_server()
+ payload = json.loads(server.tools["setup_check"]())
+
+ assert payload["web_auth"] == {
+ "paired": True,
+ "selected_wallet": "0x1111111111111111111111111111111111111111",
+ }
+ assert payload["passed"] is True
+ assert not any("No private key" in issue for issue in payload["issues"])
+
+
+def test_mcp_pair_status_uses_agent_cli_pair_status(monkeypatch):
+ install_fake_mcp(monkeypatch)
+
+ import cli.mcp_server as mcp_server
+
+ calls = []
+ monkeypatch.setattr(mcp_server, "_run_hl", lambda *args, timeout=30: calls.append(args) or "Pairing: NONE")
+
+ server = mcp_server.create_mcp_server()
+
+ assert server.tools["pair_status"]() == "Pairing: NONE"
+ assert calls == [("pair", "status")]
+
+
+def test_mcp_funding_hedge_propose_is_read_only_json(monkeypatch):
+ install_fake_mcp(monkeypatch)
+
+ from cli.mcp_server import create_mcp_server
+
+ server = create_mcp_server()
+ payload = json.loads(server.tools["funding_hedge_propose"](
+ asset="BTC",
+ perp_side="long",
+ perp_notional_usd=150_000.0,
+ funding_apr=0.45,
+ ))
+
+ assert payload["asset"] == "BTC"
+ assert payload["perp_side"] == "long"
+ assert payload["perp_notional_usd"] == 150_000.0
+ assert payload["hedge_notional_usd"] > 0
+
+
+def test_mcp_run_strategy_builds_bounded_mock_agent_cli_call(monkeypatch):
+ install_fake_mcp(monkeypatch)
+
+ import cli.mcp_server as mcp_server
+
+ calls = []
+ monkeypatch.setattr(mcp_server, "_run_hl", lambda *args, timeout=30: calls.append((args, timeout)) or "ok")
+
+ server = mcp_server.create_mcp_server()
+
+ assert server.tools["run_strategy"](
+ "engine_mm",
+ instrument="ETH-PERP",
+ tick=0,
+ max_ticks=1,
+ mock=True,
+ dry_run=True,
+ ) == "ok"
+ assert calls == [
+ (
+ (
+ "run",
+ "engine_mm",
+ "-i",
+ "ETH-PERP",
+ "-t",
+ "0",
+ "--max-ticks",
+ "1",
+ "--mock",
+ "--dry-run",
+ ),
+ 60,
+ )
+ ]
+
+
def test_mcp_money_tools_require_confirm(monkeypatch):
fastmcp_module = types.ModuleType("mcp.server.fastmcp")
fastmcp_module.FastMCP = FakeFastMCP
diff --git a/tests/test_strategy_claude_agent.py b/tests/test_strategy_ai_agent.py
similarity index 84%
rename from tests/test_strategy_claude_agent.py
rename to tests/test_strategy_ai_agent.py
index 4d7e5e7..6fc4010 100644
--- a/tests/test_strategy_claude_agent.py
+++ b/tests/test_strategy_ai_agent.py
@@ -1,4 +1,4 @@
-"""Tests for ClaudeStrategy (LLM agent) — tests helper functions only, no API calls."""
+"""Tests for AIStrategy (LLM agent) — tests helper functions only, no API calls."""
import os
import sys
import time
@@ -32,37 +32,37 @@ def _ctx(pos_qty=0.0, upnl=0.0, rpnl=0.0, reduce_only=False, safe_mode=False, ro
class TestDetectProvider:
def test_claude_model(self):
- from strategies.claude_agent import _detect_provider
+ from strategies.ai_agent import _detect_provider
assert _detect_provider("claude-haiku-4-5-20251001") == "claude"
assert _detect_provider("claude-3-sonnet") == "claude"
def test_gemini_model(self):
- from strategies.claude_agent import _detect_provider
+ from strategies.ai_agent import _detect_provider
assert _detect_provider("gemini-2.0-flash") == "gemini"
assert _detect_provider("gemini-pro") == "gemini"
def test_openai_model(self):
- from strategies.claude_agent import _detect_provider
+ from strategies.ai_agent import _detect_provider
assert _detect_provider("gpt-4o") == "openai"
assert _detect_provider("o1-mini") == "openai"
assert _detect_provider("o3-mini") == "openai"
assert _detect_provider("o4-mini") == "openai"
def test_blockrun_model(self):
- from strategies.claude_agent import _detect_provider
+ from strategies.ai_agent import _detect_provider
assert _detect_provider("blockrun/auto") == "blockrun"
assert _detect_provider("blockrun/claude-sonnet") == "blockrun"
def test_unknown_defaults_to_gemini(self):
- from strategies.claude_agent import _detect_provider
+ from strategies.ai_agent import _detect_provider
assert _detect_provider("some-random-model") == "gemini"
assert _detect_provider("") == "gemini"
class TestParseToolCall:
def _make_strat(self):
- from strategies.claude_agent import ClaudeStrategy
- return ClaudeStrategy(base_size=0.5, max_position=5.0)
+ from strategies.ai_agent import AIStrategy
+ return AIStrategy(base_size=0.5, max_position=5.0)
def test_valid_place_order(self):
strat = self._make_strat()
@@ -166,8 +166,8 @@ def test_instrument_from_snapshot(self):
class TestBuildUserMessage:
def _make_strat(self):
- from strategies.claude_agent import ClaudeStrategy
- return ClaudeStrategy(base_size=0.5, max_position=5.0)
+ from strategies.ai_agent import AIStrategy
+ return AIStrategy(base_size=0.5, max_position=5.0)
def test_contains_market_data(self):
strat = self._make_strat()
@@ -218,8 +218,8 @@ def test_no_context_uses_question_mark(self):
class TestBuildOpenAITools:
def test_format(self):
- from strategies.claude_agent import ClaudeStrategy
- strat = ClaudeStrategy()
+ from strategies.ai_agent import AIStrategy
+ strat = AIStrategy()
tools = strat._build_openai_tools()
assert len(tools) == 2
for t in tools:
@@ -229,8 +229,8 @@ def test_format(self):
assert "parameters" in t["function"]
def test_place_order_tool(self):
- from strategies.claude_agent import ClaudeStrategy
- strat = ClaudeStrategy()
+ from strategies.ai_agent import AIStrategy
+ strat = AIStrategy()
tools = strat._build_openai_tools()
place_order = [t for t in tools if t["function"]["name"] == "place_order"][0]
params = place_order["function"]["parameters"]
@@ -239,8 +239,8 @@ def test_place_order_tool(self):
assert "price" in params["properties"]
def test_hold_tool(self):
- from strategies.claude_agent import ClaudeStrategy
- strat = ClaudeStrategy()
+ from strategies.ai_agent import AIStrategy
+ strat = AIStrategy()
tools = strat._build_openai_tools()
hold = [t for t in tools if t["function"]["name"] == "hold"][0]
params = hold["function"]["parameters"]
@@ -249,34 +249,34 @@ def test_hold_tool(self):
class TestOpenRouterFusion:
def test_fusion_defaults_to_requested_route(self, monkeypatch):
- from strategies.claude_agent import ClaudeStrategy
+ from strategies.ai_agent import AIStrategy
monkeypatch.delenv("OPENROUTER_FUSION_MODEL", raising=False)
monkeypatch.delenv("NUNCHI_OPENROUTER_FUSION_MODEL", raising=False)
- strat = ClaudeStrategy(model="openrouter/fusion")
+ strat = AIStrategy(model="openrouter/fusion")
assert strat._resolve_openrouter_model() == "openrouter/fusion"
def test_fusion_can_be_overridden(self, monkeypatch):
- from strategies.claude_agent import ClaudeStrategy
+ from strategies.ai_agent import AIStrategy
monkeypatch.setenv("OPENROUTER_FUSION_MODEL", "anthropic/claude-haiku")
- strat = ClaudeStrategy(model="openrouter/fusion")
+ strat = AIStrategy(model="openrouter/fusion")
assert strat._resolve_openrouter_model() == "anthropic/claude-haiku"
def test_fusion_plugin_uses_budget_preset(self, monkeypatch):
- from strategies.claude_agent import ClaudeStrategy
+ from strategies.ai_agent import AIStrategy
monkeypatch.setenv("OPENROUTER_FUSION_PRESET", "general-budget")
- strat = ClaudeStrategy(model="openrouter/fusion")
+ strat = AIStrategy(model="openrouter/fusion")
assert strat._openrouter_fusion_plugins() == [
{"id": "fusion", "preset": "general-budget"}
]
def test_fusion_plugin_supports_explicit_panel(self, monkeypatch):
- from strategies.claude_agent import ClaudeStrategy
+ from strategies.ai_agent import AIStrategy
monkeypatch.setenv(
"OPENROUTER_FUSION_ANALYSIS_MODELS",
@@ -284,7 +284,7 @@ def test_fusion_plugin_supports_explicit_panel(self, monkeypatch):
)
monkeypatch.setenv("OPENROUTER_FUSION_JUDGE_MODEL", "openai/gpt-5-nano")
monkeypatch.setenv("OPENROUTER_FUSION_MAX_TOOL_CALLS", "1")
- strat = ClaudeStrategy(model="openrouter/fusion")
+ strat = AIStrategy(model="openrouter/fusion")
assert strat._openrouter_fusion_plugins() == [
{
@@ -299,19 +299,19 @@ def test_fusion_plugin_supports_explicit_panel(self, monkeypatch):
]
def test_force_fusion_env(self, monkeypatch):
- from strategies.claude_agent import ClaudeStrategy
+ from strategies.ai_agent import AIStrategy
monkeypatch.setenv("OPENROUTER_FORCE_FUSION", "true")
- strat = ClaudeStrategy(model="openrouter/fusion")
+ strat = AIStrategy(model="openrouter/fusion")
assert strat._force_openrouter_fusion() is True
def test_llm_decision_interval_skips_intermediate_ticks(self, monkeypatch):
- from strategies.claude_agent import ClaudeStrategy
+ from strategies.ai_agent import AIStrategy
monkeypatch.setenv("NUNCHI_LLM_DECISION_INTERVAL_TICKS", "3")
calls = []
- strat = ClaudeStrategy(model="gemini-2.0-flash")
+ strat = AIStrategy(model="gemini-2.0-flash")
def fake_call(user_msg, snapshot):
calls.append(strat._current_tick_index)
@@ -325,24 +325,24 @@ def fake_call(user_msg, snapshot):
assert calls == [1, 4]
-class TestClaudeStrategyOnTick:
+class TestAIStrategyOnTick:
def test_zero_mid_returns_empty(self):
- from strategies.claude_agent import ClaudeStrategy
- strat = ClaudeStrategy()
+ from strategies.ai_agent import AIStrategy
+ strat = AIStrategy()
orders = strat.on_tick(_snap(mid=0.0, bid=0.0, ask=0.0), _ctx())
assert orders == []
def test_safe_mode_returns_empty(self):
- from strategies.claude_agent import ClaudeStrategy
- strat = ClaudeStrategy()
+ from strategies.ai_agent import AIStrategy
+ strat = AIStrategy()
orders = strat.on_tick(_snap(), _ctx(safe_mode=True))
assert orders == []
def test_on_tick_without_api_key_returns_empty(self):
"""Without API keys, on_tick should catch the error and return []."""
- from strategies.claude_agent import ClaudeStrategy
+ from strategies.ai_agent import AIStrategy
# Use gemini model (default) — no API key set
- strat = ClaudeStrategy(model="gemini-2.0-flash")
+ strat = AIStrategy(model="gemini-2.0-flash")
# Clear any env vars
old_key = os.environ.pop("GEMINI_API_KEY", None)
old_key2 = os.environ.pop("GOOGLE_API_KEY", None)
diff --git a/tests/test_strategy_registry.py b/tests/test_strategy_registry.py
index a72afd8..5e3d74e 100644
--- a/tests/test_strategy_registry.py
+++ b/tests/test_strategy_registry.py
@@ -31,9 +31,9 @@ def test_error_shows_available(self):
with pytest.raises(ValueError, match="simple_mm"):
resolve_strategy_path("bad_name")
- def test_claude_agent_registered(self):
- path = resolve_strategy_path("claude_agent")
- assert "ClaudeStrategy" in path
+ def test_ai_agent_registered(self):
+ path = resolve_strategy_path("ai_agent")
+ assert "AIStrategy" in path
def test_registry_has_params(self):
for name, entry in STRATEGY_REGISTRY.items():