This guide covers every method for pulling data from a Nunchi-hosted agent. Hosted agents are provisioned only through web-auth after billing entitlement and wallet binding.
| 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) |
Security: Hosted deployment is gated by web-auth billing entitlement and wallet binding. Mutating control endpoints on the hosted runner require operator-provisioned control tokens; users should interact through web-auth and the scoped hosted MCP flow instead of direct infrastructure access.
Deploy hosted agents from web-auth:
https://auth.nunchi.trade
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.
Throughout this document, $AGENT_URL refers to your agent's base URL:
# Hosted endpoint returned by web-auth after deployment
export AGENT_URL=https://your-agent-hosted-endpoint.example
# Verify connectivity
curl $AGENT_URL/healthHealth check endpoint. Use this to verify the agent is reachable and the trading process is alive.
curl $AGENT_URL/healthResponse:
{
"status": "ok",
"mode": "apex",
"uptime_s": 3842,
"pid": 127,
"alive": true
}| Field | Type | Description |
|---|---|---|
status |
string | Always "ok" if server is responding |
mode |
string | RUN_MODE env var: "apex", "strategy", or "mcp" |
uptime_s |
int | Seconds since the entrypoint started |
pid |
int|null | PID of the child trading process |
alive |
bool | Whether the child process is still running |
Notes:
- Used by Nunchi's hosted-agent provisioner and health monitoring.
- Returns 200 if the HTTP server is responding; check
aliveto distinguish a live trading process from an unhealthy child process.
Primary status endpoint. Returns the agent's current state, positions, and PnL.
curl $AGENT_URL/api/statusResponse (APEX mode):
{
"status": "running",
"engine": "apex",
"tick_count": 284,
"daily_pnl": -12.50,
"total_pnl": 342.00,
"total_trades": 28,
"max_slots": 3,
"active_slots": [
{
"slot_id": 0,
"instrument": "VXX-USDYP",
"side": "long",
"entry_size": 2.5,
"entry_price": 30.552,
"roe_pct": 4.2,
"dsl_phase": 2,
"status": "active"
}
],
"closed_slots": [],
"positions": [
{
"slot": 0,
"market": "VXX-USDYP",
"side": "long",
"size": 2.5,
"entry": 30.552,
"roe": 4.2,
"phase": 2
}
]
}Response (single-strategy mode):
{
"status": "running",
"engine": "engine_mm",
"tick_count": 1042,
"instrument": "ETH-PERP",
"position_qty": 0.5,
"unrealized_pnl": 3.42,
"realized_pnl": 18.90,
"total_orders": 312,
"total_fills": 198
}Response (agent not running):
{
"status": "stopped"
}| Field | Mode | Type | Description |
|---|---|---|---|
status |
Both | string | "running" or "stopped" |
engine |
Both | string | Strategy ID ("apex", "engine_mm", etc.) |
tick_count |
Both | int | Number of evaluation cycles completed |
daily_pnl |
APEX | float | Today's profit/loss in USD |
total_pnl |
APEX | float | Cumulative profit/loss in USD |
total_trades |
APEX | int | Number of executed trades |
max_slots |
APEX | int | Maximum concurrent positions |
active_slots |
APEX | array | Currently open position details (full slot objects) |
closed_slots |
APEX | array | Last 5 closed positions |
positions |
APEX | array | Simplified position array for UI consumption |
instrument |
Strategy | string | Trading instrument (e.g., "ETH-PERP") |
position_qty |
Strategy | float | Net position quantity (positive = long) |
unrealized_pnl |
Strategy | float | Mark-to-market PnL |
realized_pnl |
Strategy | float | Locked PnL from closed trades |
total_orders |
Strategy | int | Orders placed this session |
total_fills |
Strategy | int | Orders that filled |
Data source: Reads from $DATA_DIR/apex/state.json (APEX mode) or $DATA_DIR/cli/state.db (strategy mode). These files are written by the trading engine after every tick.
Fallback behavior: If the Python status reader fails, the Node.js server reads state.json directly as a fallback. The Python entrypoint has no fallback.
Returns the full catalog of available trading strategies and YEX markets.
curl $AGENT_URL/api/strategiesResponse:
{
"strategies": {
"simple_mm": {
"description": "Fixed-spread market maker",
"params": { "spread_bps": 10, "base_size": 0.5 }
},
"avellaneda_mm": {
"description": "Avellaneda-Stoikov inventory-aware market maker",
"params": { "gamma": 0.1, "k": 1.5, "base_size": 0.5, "max_inventory": 5 }
},
"engine_mm": { "..." : "..." },
"regime_mm": { "..." : "..." },
"grid_mm": { "..." : "..." },
"liquidation_mm": { "..." : "..." },
"funding_arb": { "..." : "..." },
"basis_arb": { "..." : "..." },
"momentum_breakout": { "..." : "..." },
"mean_reversion": { "..." : "..." },
"aggressive_taker": { "..." : "..." },
"hedge_agent": { "..." : "..." },
"rfq_agent": { "..." : "..." },
"claude_agent": { "..." : "..." }
},
"markets": {
"VXX-USDYP": "Volatility index yield perpetual",
"US3M-USDYP": "US 3-month Treasury rate yield perpetual",
"BTCSWP-USDYP": "BTC interest rate swap yield perpetual"
}
}Each strategy includes its description, type, and params with default values. Use this to populate strategy selection UI or validate configuration inputs.
Human-readable plain-text status. Calls hl apex status internally.
curl $AGENT_URL/statusResponse:
APEX Orchestrator — default preset
Tick: 284 | Daily PnL: -$12.50 | Slots: 1/3
Slot 0: VXX-USDYP LONG 2.5 @ 30.552 | ROE: +4.2% | Phase: Tier 2
Slot 1: (empty)
Slot 2: (empty)
Same output as running hl apex status in a terminal. Useful for quick checks but not suitable for programmatic consumption — use /api/status instead.
Verifies that the agent has the Nunchi trading CLI installed and returns the strategy count.
curl -X POST $AGENT_URL/api/skill/install \
-H "Content-Type: application/json"Response:
{
"installed": true,
"strategies": 14,
"tools": 13
}Error response:
{
"installed": false,
"error": "ModuleNotFoundError: No module named 'cli'"
}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.
curl -X POST $AGENT_URL/api/pause \
-H "Authorization: Bearer $API_AUTH_TOKEN"Response:
{ "status": "paused" }Error (no process running):
{ "error": "No running agent to pause" }Error (token not configured):
{ "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.
Resumes a paused trading process by sending SIGCONT.
curl -X POST $AGENT_URL/api/resume \
-H "Authorization: Bearer $API_AUTH_TOKEN"Response:
{ "status": "resumed" }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, X-API-Token
OPTIONS requests to any path return 204 with these headers. Set the CORS_ORIGIN environment variable to restrict origins in production.
Server-Sent Events stream that pushes the agent's status every time the tick counter changes. The server polls the state file every 2 seconds and emits an event only when tick_count has advanced.
JavaScript:
const source = new EventSource(`${AGENT_URL}/api/feed`);
source.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(`Tick ${data.tick_count}:`, data);
};
source.onerror = () => {
console.log('SSE connection lost, will auto-reconnect');
};curl (for testing):
curl -N $AGENT_URL/api/feedEvent format:
data: {"status":"running","engine":"apex","tick_count":285,"daily_pnl":-11.20,...}
data: {"status":"running","engine":"apex","tick_count":286,"daily_pnl":-10.80,...}
Each data: line contains the same JSON payload as GET /api/status. Events are separated by double newlines per the SSE specification.
Headers returned:
Content-Type: text/event-stream
Cache-Control: no-cache
X-Accel-Buffering: no
The X-Accel-Buffering: no header prevents Nginx reverse proxies from buffering the stream.
Behavior:
- Emits immediately on first connect (current state)
- Then emits only when
tick_countchanges (de-duplicated) - APEX ticks every 60 seconds by default, so events arrive roughly every 60s
- Single-strategy ticks are configurable (default 10s), so events arrive more frequently
- The connection stays open indefinitely until the client disconnects
EventSourceauto-reconnects on connection loss (browser default behavior)
Python example:
import json
import requests
response = requests.get(f"{AGENT_URL}/api/feed", stream=True)
for line in response.iter_lines():
if line:
text = line.decode("utf-8")
if text.startswith("data: "):
payload = json.loads(text[6:])
print(f"Tick {payload['tick_count']}: PnL ${payload.get('daily_pnl', 0):.2f}")The leaderboard runs as a separate microservice from the agent. It tracks registered wallet addresses and queries Hyperliquid directly for account values.
The leaderboard is operated as Nunchi infrastructure. This repository does not publish a public Docker or Railway deployment path for it.
| Variable | Default | Description |
|---|---|---|
LEADERBOARD_PORT |
8090 |
HTTP server port |
LEADERBOARD_DB |
/data/leaderboard.db |
SQLite database path |
CORS_ORIGIN |
* |
Allowed CORS origin |
curl http://localhost:8090/health{ "status": "ok", "uptime_s": 1200 }Register a wallet address for leaderboard tracking. The service queries Hyperliquid at registration time to capture the initial_account_value baseline.
curl -X POST http://localhost:8090/api/register \
-H "Content-Type: application/json" \
-d '{
"address": "0x1234567890abcdef1234567890abcdef12345678",
"network": "testnet",
"display_name": "my-agent"
}'| Param | Type | Required | Description |
|---|---|---|---|
address |
string | Yes | Ethereum address (0x + 40 hex chars) |
network |
string | No | "testnet" (default) or "mainnet" |
display_name |
string | No | Display name (max 32 chars) |
Response (new registration):
{
"registered": true,
"new": true,
"address": "0x1234567890abcdef1234567890abcdef12345678",
"network": "testnet",
"initial_account_value": 10000.0
}Response (already registered):
{
"registered": true,
"new": false,
"address": "0x1234567890abcdef1234567890abcdef12345678",
"network": "testnet",
"initial_account_value": 10000.0
}Error (invalid address):
{ "error": "Invalid Ethereum address (expected 0x + 40 hex chars)" }Error (HL query failed):
{ "error": "Failed to query Hyperliquid: ConnectionError(...)" }How PnL is computed:
PnL = current_account_value - initial_account_value. Theinitial_account_valueis captured once at registration and never updated. If the user deposits or withdraws funds after registration, the PnL figure will be incorrect. This is a known limitation.
Returns the ranked leaderboard for a given network.
curl "http://localhost:8090/api/leaderboard?network=testnet"| Param | Type | Default | Description |
|---|---|---|---|
network |
string | testnet |
"testnet" or "mainnet" |
Response:
{
"agents": [
{
"rank": 1,
"address": "0xabcd...1234",
"display_name": "alpha-apex",
"pnl": 842.50,
"account_value": 10842.50,
"positions_count": 2,
"network": "testnet",
"registered_at": 1709712000000
},
{
"rank": 2,
"address": "0xef01...5678",
"display_name": "",
"pnl": 321.00,
"account_value": 10321.00,
"positions_count": 1,
"network": "testnet",
"registered_at": 1709798400000
}
],
"total_agents": 2,
"last_updated": 1709884800000
}| Field | Type | Description |
|---|---|---|
agents |
array | Agents sorted by PnL (descending) |
agents[].rank |
int | 1-indexed rank |
agents[].address |
string | Wallet address (lowercase) |
agents[].display_name |
string | User-chosen name (may be empty) |
agents[].pnl |
float | current_value - initial_value (rounded to 2 decimals) |
agents[].account_value |
float | Current HL account value |
agents[].positions_count |
int | Number of open positions on HL |
agents[].network |
string | "testnet" or "mainnet" |
agents[].registered_at |
int | Registration timestamp (Unix ms) |
agents[].stale |
bool | Present and true if the HL query failed for this agent |
total_agents |
int | Total registered agents on this network |
last_updated |
int | Cache refresh timestamp (Unix ms) |
Caching: The leaderboard is cached and refreshed by a background thread every 45 seconds. The last_updated field tells you how fresh the data is. The cache refreshes both testnet and mainnet every cycle.
Frontend polling:
const LEADERBOARD_URL = 'https://leaderboard.nunchi.trade';
async function fetchLeaderboard(network = 'testnet') {
const res = await fetch(`${LEADERBOARD_URL}/api/leaderboard?network=${network}`);
const data = await res.json();
return data.agents;
}
// Poll every 30 seconds
setInterval(() => fetchLeaderboard().then(renderTable), 30000);CLI usage:
# 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 8090The MCP server exposes 16 tools for AI agent orchestration via the Model Context Protocol. This is the access path for Claude Code, OpenClaw, or any MCP-compatible client.
# stdio transport (for Claude Code / local AI agents)
hl mcp serve
# SSE transport (for development harnesses)
hl mcp serve --transport sseHosted users should use the web-auth hosted MCP connection instead of running their own remote MCP deployment.
Add to your Claude Code MCP configuration:
{
"mcpServers": {
"nunchi": {
"command": "hl",
"args": ["mcp", "serve"]
}
}
}These execute directly in Python with no subprocess overhead.
List all 14 trading strategies with descriptions and default parameters.
Tool: strategies
Args: (none)
Returns JSON with strategy catalog and YEX markets. Same data as GET /api/strategies.
Get builder fee configuration.
Tool: builder_status
Args: (none)
Returns:
{
"enabled": true,
"builder_address": "0x...",
"fee_bps": 10,
"fee_rate_tenths_bps": 100,
"max_fee_rate_str": "0.1%"
}List saved encrypted keystores.
Tool: wallet_list
Args: (none)
Returns array of keystore file paths and addresses, or "No keystores found.".
Create a new Ethereum wallet non-interactively. Generates a random private key, encrypts it with a random password, and saves to ~/.hl-agent/keystore/.
Tool: wallet_auto
Args: { "save_env": true }
Returns:
{
"address": "0x...",
"password": "random-32-char-token",
"keystore": "/home/user/.hl-agent/keystore/0x....json",
"env_file": "/home/user/.hl-agent/env"
}Validate that the environment is correctly configured for trading.
Tool: setup_check
Args: (none)
Returns:
{
"ok": [
"hyperliquid-python-sdk installed",
"HL_PRIVATE_KEY set",
"Network: testnet",
"Builder fee: 10 bps"
],
"issues": [],
"passed": true
}These shell out to the CLI and may take significant time.
Get Hyperliquid account state (balances, margins, positions).
Tool: account
Args: { "mainnet": false }
Returns human-readable account summary from hl account.
Show current positions, PnL, and risk state.
Tool: status
Args: (none)
Returns human-readable status from hl status.
Place a single manual order (IOC).
Tool: trade
Args: { "instrument": "ETH-PERP", "side": "buy", "size": 0.5 }
Executes hl trade ETH-PERP buy 0.5. Returns fill confirmation or error.
Start autonomous trading with a named strategy.
Tool: run_strategy
Args: {
"strategy": "avellaneda_mm",
"instrument": "ETH-PERP",
"tick": 10,
"max_ticks": 100,
"mock": false,
"dry_run": false,
"mainnet": false
}
This is a long-running call. If max_ticks is set, it returns after completion. Without max_ticks, it runs indefinitely (set a timeout on your MCP client).
Run the opportunity radar once — screen all HL perps for trading setups.
Tool: radar_run
Args: { "mock": false }
Returns scored opportunities with market structure, technicals, and funding analysis.
Get APEX orchestrator status (slots, positions, daily PnL).
Tool: apex_status
Args: (none)
Start the APEX multi-slot orchestrator.
Tool: apex_run
Args: {
"preset": "default",
"mock": true,
"max_ticks": 50,
"mainnet": false
}
Long-running. Timeout is computed as max(120, max_ticks * 60 + 30) seconds.
Run REFLECT performance review — analyze trades, compute metrics, generate recommendations.
Tool: reflect_run
Args: { "since": "2026-03-01" }
Returns detailed performance report with win rate, FDR, direction analysis, and parameter adjustment recommendations.
These access the agent's accumulated knowledge and trade records.
Read agent memory — learnings, parameter changes, market observations.
Tool: agent_memory
Args: { "query_type": "playbook", "limit": 20 }
| Arg | Values | Description |
|---|---|---|
query_type |
"recent", "playbook" |
Recent events or accumulated knowledge |
limit |
int | Max events (default 20) |
event_type |
"param_change", "reflect_review", "notable_trade", "judge_finding", "session_start", "session_end" |
Filter by type |
Read trade journal — structured position records with entry/exit reasoning.
Tool: trade_journal
Args: { "date": "2026-03-06", "limit": 10 }
Returns journal entries with signal source, entry reasoning, exit reasoning, close reason, and quality rating.
Get latest signal quality evaluation — false positive rates, accuracy by instrument, config recommendations.
Tool: judge_report
Args: (none)
Returns the most recent Judge analysis, or {"status": "no_reports"} if APEX hasn't run long enough to generate one.
Read trading context from Obsidian vault — watchlists, market theses, risk preferences.
Tool: obsidian_context
Args: (none)
Requires an Obsidian vault at ~/obsidian-vault. Returns {"status": "unavailable"} if not found.
If you have filesystem access to the agent (SSH, mounted volume, same container), you can read the state files directly without going through the API.
cat $DATA_DIR/apex/state.json | python -m json.toolContains: tick_count, slots[], daily_pnl, total_pnl, total_trades, preset, config.
# Last 10 trades
tail -10 $DATA_DIR/apex/trades.jsonlEach line is a JSON object:
{"tick":42,"oid":"abc123","instrument":"VXX-USDYP","side":"buy","price":"30.55","quantity":"2.5","timestamp_ms":1709712000000,"fee":"0.0345","strategy":"engine_mm"}Financial values are strings to preserve decimal precision.
sqlite3 $DATA_DIR/cli/state.db "SELECT key, value FROM kv"Keys: tick_count, positions, risk, start_time_ms, strategy_id, instrument, order_stats.
cat $DATA_DIR/radar/scan-history.json | python -m json.toolcat $DATA_DIR/movers/scan-history.json | python -m json.toolls $DATA_DIR/reflect/
# Reports are saved as markdown files with timestamps
cat $DATA_DIR/reflect/report-latest.mdtail -5 $DATA_DIR/journal/entries.jsonlimport time, requests
AGENT = "https://your-agent.up.railway.app"
while True:
r = requests.get(f"{AGENT}/api/status")
data = r.json()
if data["status"] == "running":
print(f"Tick {data['tick_count']} | PnL: ${data.get('daily_pnl', 0):.2f}")
for pos in data.get("positions", []):
print(f" {pos['market']} {pos['side']} {pos['size']} @ {pos['entry']} → ROE {pos['roe']:.1f}%")
else:
print("Agent stopped")
time.sleep(30)import json, requests
AGENT = "https://your-agent.up.railway.app"
with requests.get(f"{AGENT}/api/feed", stream=True) as r:
for line in r.iter_lines(decode_unicode=True):
if line and line.startswith("data: "):
event = json.loads(line[6:])
tick = event.get("tick_count", 0)
pnl = event.get("daily_pnl", 0)
positions = event.get("positions", [])
print(f"[tick {tick}] PnL=${pnl:.2f} | {len(positions)} positions open")import requests
agents = [
{"name": "apex-alpha", "url": "https://apex-alpha.up.railway.app"},
{"name": "apex-beta", "url": "https://apex-beta.up.railway.app"},
]
for agent in agents:
try:
r = requests.get(f"{agent['url']}/api/status", timeout=5)
data = r.json()
pnl = data.get("daily_pnl", data.get("realized_pnl", 0))
print(f"{agent['name']}: {data['status']} | tick {data.get('tick_count', 0)} | PnL ${pnl:.2f}")
except Exception as e:
print(f"{agent['name']}: UNREACHABLE ({e})")import requests
AGENT = "https://your-agent.up.railway.app"
LEADERBOARD = "https://leaderboard.nunchi.trade"
YOUR_ADDRESS = "0x1234...abcd"
# Get your agent status
status = requests.get(f"{AGENT}/api/status").json()
# Get leaderboard
lb = requests.get(f"{LEADERBOARD}/api/leaderboard?network=testnet").json()
# Find your rank
your_entry = next((a for a in lb["agents"] if a["address"] == YOUR_ADDRESS.lower()), None)
if your_entry:
print(f"Rank #{your_entry['rank']} of {lb['total_agents']} | PnL: ${your_entry['pnl']:.2f}")
else:
print("Not registered on leaderboard")| Method | Path | Body | Response | Latency |
|---|---|---|---|---|
GET |
/health |
None | JSON | <10ms |
GET |
/api/status |
None | JSON | <100ms |
GET |
/api/strategies |
None | JSON | <100ms |
GET |
/api/feed |
None | SSE stream | Persistent |
GET |
/status |
None | Plain text | <1s |
POST |
/api/skill/install |
None | JSON | <2s |
POST |
/api/pause |
Auth header | JSON | <10ms |
POST |
/api/resume |
Auth header | JSON | <10ms |
| Method | Path | Body | Response | Latency |
|---|---|---|---|---|
GET |
/health |
None | JSON | <10ms |
GET |
/api/leaderboard?network= |
None | JSON | <10ms (cached) |
POST |
/api/register |
JSON | JSON | 1-5s (queries HL) |
OPTIONS |
* |
None | 204 | <10ms |
| Tool | Type | Latency | Side Effects |
|---|---|---|---|
strategies |
Fast | <100ms | None |
builder_status |
Fast | <100ms | None |
wallet_list |
Fast | <100ms | None |
wallet_auto |
Fast | <500ms | Creates keystore file |
setup_check |
Fast | <100ms | None |
account |
Subprocess | 1-5s | None |
status |
Subprocess | <1s | None |
trade |
Subprocess | 1-5s | Places order on HL |
run_strategy |
Subprocess | Minutes+ | Runs trading loop |
radar_run |
Subprocess | 10-30s | None |
apex_status |
Subprocess | <1s | None |
apex_run |
Subprocess | Minutes+ | Runs APEX loop |
reflect_run |
Subprocess | 5-15s | None |
agent_memory |
Fast | <100ms | None |
trade_journal |
Fast | <100ms | None |
judge_report |
Fast | <100ms | None |
obsidian_context |
Fast | <100ms | None |