Bot API Reference
opentrader-bot exposes an HTTP API on port 8000 (internal Docker network — not publicly accessible).
Base URL (from inside the openclaw container): http://opentrader-bot:8000
Summary
| Section | Endpoints |
|---|---|
| Health | GET /health |
| License | GET /license/status, POST /license/register, POST /license/activate |
| Dashboard | GET /dashboard, POST /api/agent/{name}, GET /api/state |
| News | GET /api/news |
| Portfolio | GET /api/portfolio |
| Bot Actions | GET /scan, POST /trade, GET /custom_check, GET /trailing, GET /status, POST /api/closeall, POST /api/reset-daily |
| Telegram | GET /api/notify |
| Trade Management | GET /api/trades, GET /api/trades/recover, POST /api/trades/restore |
| Signal Flow | POST /api/scan-and-signal, POST /api/signal, GET /api/signal/list, GET /api/signal/pending, GET /api/signal/{symbol}, POST /api/signal/{symbol}/confirm, POST /api/signal/{symbol}/reject, POST /api/signal/cleanup |
Health
GET /health
Health check. Always available, no license required.
Response
{
"ok": true,
"license_status": "active",
"plan": "free",
"commit": "a1b2c3d"
}
commitis the 7-character git SHA baked in at Docker build time (GIT_COMMITbuild arg). Shows"dev"on local builds.
License
GET /license/status
Returns the current license state of this machine.
Response (active)
{
"status": "active",
"plan": "free",
"features": {},
"expires_at": null,
"validated_at": "2026-04-18T10:00:00",
"machine_id": "abc123"
}
Response (unlicensed)
{
"status": "unlicensed",
"plan": null,
"features": {},
"message": "Chưa có license. Dùng /license/register hoặc /license/activate."
}
POST /license/register
Register and receive a free license key. The key is automatically activated and saved to /app/state/license.json.
Request body
{ "email": "[email protected]", "name": "Trader Name" }
Response
{
"ok": true,
"license_key": "OT-XXXX-XXXX-XXXX-XXXX",
"plan": "free",
"message": "License đã được kích hoạt thành công! Bot sẵn sàng hoạt động."
}
POST /license/activate
Activate an existing license key (e.g., after resetting the machine).
Request body
{ "license_key": "OT-XXXX-XXXX-XXXX-XXXX" }
Response
{
"ok": true,
"plan": "free",
"message": "License kích hoạt thành công — plan=free."
}
Dashboard
GET /dashboard
Serves the web dashboard HTML (app/static/dashboard.html).
POST /api/agent/{name}
An agent reports its current status. name must be one of: coo, ops, tech, finance, scout.
Request body
{ "status": "running", "action": "Scan ta_trend" }
status values: idle | running | waiting | error
Response
{ "ok": true }
GET /api/state
Returns the full dashboard state (all agent statuses + activity log). Polled every 3 seconds by the dashboard.
Response
{
"agents": {
"coo": { "status": "idle", "action": "Chờ lệnh...", "updated": "10:45:02" },
"ops": { "status": "idle", "action": "Chờ lệnh...", "updated": "10:45:02" },
"tech": { "status": "running", "action": "Scan ta_trend", "updated": "10:46:01" },
"finance": { "status": "idle", "action": "Chờ lệnh...", "updated": "10:45:02" },
"scout": { "status": "idle", "action": "Chờ lệnh...", "updated": "10:45:02" }
},
"log": [
{ "time": "10:46:01", "agent": "tech", "action": "Scan ta_trend", "status": "running" }
],
"ts": "10:46:04"
}
News
GET /api/news
Proxy crypto news from RSS sources. Tries each source in sequence and returns the first with valid items. Result is cached for 10 minutes. No license required.
Response (success)
{
"ok": true,
"data": [
{ "title": "Bitcoin hits new high", "url": "https://...", "source": "cointelegraph.com" }
],
"cached": false,
"source": "cointelegraph.com"
}
Response (cached)
{ "ok": true, "data": [...], "cached": true }
Response (all sources failed)
{ "ok": false, "data": [], "error": "cointelegraph.com: timed out | coindesk.com: ..." }
Portfolio
GET /api/portfolio
Fetch current balance from the active exchange. No license required.
Response (success)
{
"ok": true,
"balance": 1042.50,
"currency": "USDT",
"holdings": [{ "asset": "USDT", "qty": 1042.50, "value": 1042.50, "price": 1.0 }],
"exchange": "binance",
"testnet": false
}
Response (error)
{
"ok": false,
"balance": null,
"currency": "—",
"exchange": "—",
"error": "Invalid API key"
}
Bot Actions (require license)
All bot endpoints require a valid license. Returns 503 if unlicensed.
The agent query parameter is optional (default shown below) — it controls which agent card on the dashboard reflects the action.
GET /scan
Scan the watchlist using TA indicators. Returns candidates ready to trade.
Query params
| Param | Required | Default | Values |
|---|---|---|---|
bot | yes | — | ta_trend, ta_mean_revert |
agent | no | tech | coo, ops, tech, finance, scout |
Example
curl "http://opentrader-bot:8000/scan?bot=ta_trend"
Response
{
"status": "ok",
"exchange": "hyperliquid",
"testnet": false,
"bot": "ta_trend",
"scanned": 12,
"candidates": [
{ "symbol": "ETHUSDT", "direction": "buy", "score": 6 }
],
"all": [...],
"trades_today": 1,
"remaining": 2
}
POST /trade
Place a bracket order (entry + SL + TP) on the active exchange.
Query params
| Param | Required | Description |
|---|---|---|
symbol | yes | e.g. ETHUSDT |
direction | yes | buy or sell |
bot | yes | ta_trend or ta_mean_revert |
sl | yes | Stop-loss % (e.g. 3.0) |
tp | yes | Take-profit % (e.g. 7.0) |
ev | yes | Expected value from Finance AI confirm — must be > 25 |
confidence | yes | Confidence from Finance AI confirm — must be >= 8 |
agent | no | Default: ops |
Both ev and confidence are validated server-side. Returns 422 if either threshold is not met — the trade is blocked regardless of what the agent intended.
Example
curl -X POST "http://opentrader-bot:8000/trade?symbol=ETHUSDT&direction=buy&bot=ta_trend&sl=3.0&tp=7.0&ev=32.5&confidence=8"
Response
{
"symbol": "ETHUSDT",
"direction": "buy",
"bot": "ta_trend",
"size": 0.05,
"entry_px": 2450.5,
"sl_px": 2377.0,
"tp_px": 2622.0,
"sl_pct": 3.0,
"tp_pct": 7.0,
"sl_placed": "ok",
"tp_placed": "ok",
"date": "2026-04-18"
}
GET /custom_check
Run custom indicator checks on a specific symbol and direction. Returns a checklist used by Finance AI to confirm before trading.
Query params
| Param | Required | Description |
|---|---|---|
symbol | yes | e.g. ETHUSDT |
direction | yes | buy or sell |
bot | yes | ta_trend or ta_mean_revert |
agent | no | Default: tech |
Example
curl "http://opentrader-bot:8000/custom_check?symbol=ETHUSDT&direction=buy&bot=ta_trend"
Response
{
"symbol": "ETHUSDT",
"direction": "buy",
"bot": "ta_trend",
"signal": "buy",
"passed": 5,
"total": 7,
"checklist": [
{ "name": "RSI", "pass": true, "detail": "RSI=42 < 50" },
{ "name": "MACD", "pass": true, "detail": "bullish crossover" }
]
}
GET /trailing
Update trailing stop-loss for all open positions based on current price.
Query params
| Param | Required | Default |
|---|---|---|
agent | no | tech |
Example
curl "http://opentrader-bot:8000/trailing"
Response
{
"updated": [
{ "symbol": "ETHUSDT", "old_sl": 2377.0, "new_sl": 2410.0, "profit_pct": 2.5 }
]
}
GET /status
Returns today’s trade count, consecutive losses, recent trades, and bot config.
Query params
| Param | Required | Default |
|---|---|---|
agent | no | coo |
Example
curl "http://opentrader-bot:8000/status"
Response
{
"exchange": "hyperliquid",
"testnet": false,
"trades_today": 1,
"consecutive_losses": 0,
"recent_trades": [...],
"config": { "max_trades": 3, "sl_pct": 3.0, "tp_pct": 7.0 }
}
POST /api/closeall
Cancel all SL/TP orders and close all open positions at market. Positions successfully closed are removed from state.
Query params
| Param | Required | Default |
|---|---|---|
agent | no | ops |
Example
curl -X POST "http://opentrader-bot:8000/api/closeall?agent=ops"
Response
{
"status": "ok",
"count": 2,
"closed": [
{ "symbol": "ETHUSDT", "status": "closed", "size": 0.05 },
{ "symbol": "BTCUSDT", "status": "closed", "size": 0.001 }
]
}
If a position fails to close, it remains in state with "status": "error":
{ "symbol": "SOLUSDT", "status": "error", "error": "Insufficient balance" }
Hyperliquid: closes via IOC limit order at ±5% slippage.
Binance: cancels OCO pair then places MARKET order.
POST /api/reset-daily
Reset the trades_today counter to 0, allowing the bot to place new trades within the same day. Existing open positions and state are preserved — trailing stop continues to work normally.
Example
curl -X POST "http://opentrader-bot:8000/api/reset-daily"
Response
{ "ok": true, "trades_today_before": 3, "trades_today_after": 0 }
Use this when the daily limit has been reached but you want to allow additional trades (e.g., after manually closing positions or adjusting strategy).
Telegram
GET /api/notify
Send a raw Telegram message to the boss chat. No license required.
Query params
| Param | Required | Description |
|---|---|---|
message | yes | Text to send |
Example
curl -sG "http://opentrader-bot:8000/api/notify" \
--data-urlencode "message=Bot khởi động xong ✅"
Response
{ "ok": true }
Requires
TELEGRAM_BOT_TOKEN_COOandTELEGRAM_CHAT_ID_COOin.env.
Trade Management
GET /api/trades
Returns all trades placed today (resets at midnight). Used by Scout to check for existing positions before sending a new signal.
Data is read directly from
opentrader_state.json. If Finance placed a trade today, it appears here until midnight.
Example
curl "http://opentrader-bot:8000/api/trades"
Response
{
"trades": [
{
"symbol": "ETHUSDT",
"direction": "buy",
"bot": "ta_trend",
"exchange": "hyperliquid",
"size": 0.05,
"entry_px": 2450.5,
"sl_px": 2377.0,
"tp_px": 2622.0,
"sl_pct": 3.0,
"tp_pct": 7.0,
"date": "2026-04-18"
}
],
"count": 1
}
Used by POST /api/signal for automatic filtering:
| Condition | Skip status returned |
|---|---|
Signal already pending for symbol | duplicate_skipped |
| A trade already exists for symbol today | trade_exists_skipped |
Direction is sell but no trade exists for symbol | no_position_sell_skipped |
Scout treats all three statuses identically — silently skips the candidate.
GET /api/trades/recover
Auto-recover trade records from open OCO orders on the exchange. Use when the state file is lost (container restart, volume migration) — trailing stop will resume correctly after recovery.
How it works: Scans the full watchlist, finds open OCO (SL+TP) orders, reconstructs entry_px, sl_oid, tp_oid, and size from exchange order history, then writes them to the state file.
Example
curl "http://opentrader-bot:8000/api/trades/recover"
Response
{
"ok": true,
"recovered": 2,
"trades": [
{
"symbol": "DOGEUSDT",
"direction": "buy",
"entry_px": 0.1012,
"sl_px": 0.0981,
"tp_px": 0.1082,
"size": 4696.02,
"sl_oid": 10293847,
"tp_oid": 10293848,
"sl_placed": "ok",
"source": "recovered",
"date": "2026-04-18"
}
]
}
Only adds symbols not already in state — does not overwrite actively tracked positions.
POST /api/trades/restore
Manually restore a single trade record into state. Use when you need to enter sl_oid / entry_px by hand (e.g., a position opened outside the system).
Request body
{
"symbol": "BNBUSDT",
"direction": "buy",
"size": 1.5,
"entry_px": 598.0,
"sl_px": 580.0,
"tp_px": 638.0,
"sl_oid": 10293001,
"tp_oid": 10293002,
"bot": "ta_trend"
}
| Field | Required | Description |
|---|---|---|
symbol | yes | e.g. BNBUSDT |
direction | yes | buy or sell |
size | yes | Coin quantity |
entry_px | yes | Filled entry price |
sl_px | yes | Stop-loss price |
tp_px | yes | Take-profit price |
sl_oid | no | SL order ID on exchange (required for trailing to work) |
tp_oid | no | TP order ID on exchange |
bot | no | Default: ta_trend |
Response
{ "ok": true, "trade": { "symbol": "BNBUSDT", "sl_placed": "ok", "..." } }
Signal Flow
The Scout → COO → Boss → Finance pipeline for human-in-the-loop confirmation.
Scout: POST /api/scan-and-signal ← scan + signal creation in one call
↓
Telegram notify boss (per signal)
↓
Boss replies "YES SYMBOL"
↓
COO: GET /api/signal/{symbol} ← returns 410 if > 5 min
↓
Finance: AI confirm (EV/confidence) → POST /trade
POST /api/scan-and-signal
Run a full TA scan and automatically create signals for all qualifying candidates. The bot handles scanning, filtering, signal creation, and Telegram notification in one call — Scout does not need to loop through candidates manually.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
bot | string | yes | ta_trend or ta_mean_revert |
agent | string | no | Calling agent name (default: scout) |
Example
curl -X POST "http://opentrader-bot:8000/api/scan-and-signal?bot=ta_trend&agent=scout"
Response
{
"ok": true,
"signaled": ["BTCUSDT", "ETHUSDT"],
"skipped": [
{"symbol": "SOLUSDT", "reason": "duplicate_skipped"}
],
"scan": {
"bot": "ta_trend",
"scanned": 10,
"trades_today": 1,
"remaining": 2
}
}
If the daily trade limit or consecutive loss limit has already been reached, the bot skips the scan entirely and returns:
{
"ok": true,
"signaled": [],
"skipped": [],
"reason": "daily_limit_reached"
}
skipped reasons
| Reason | Meaning |
|---|---|
duplicate_skipped | Signal already pending for this symbol |
trade_exists_skipped | A trade for this symbol exists today |
no_position_sell_skipped | SELL signal but no open position |
telegram_failed | Signal created but Telegram notification failed |
POST /api/signal
Scout submits a new pending signal. Sends a Telegram message to boss with the signal summary for review.
Duplicate signals for the same symbol (while pending) are silently skipped.
Request body
{
"symbol": "ETHUSDT",
"direction": "buy",
"bot": "ta_trend",
"price": 2450.5,
"sl_pct": 3.0,
"tp_pct": 7.0,
"checklist": [
{ "name": "RSI", "pass": true, "detail": "RSI=42" }
],
"passed": 5,
"total": 7
}
Response
{ "ok": true, "symbol": "ETHUSDT", "status": "pending" }
Signal status lifecycle: pending → confirmed | skipped | expired
GET /api/signal/list
List all signals (any status). Intended for internal / debugging use.
Response
{
"signals": [
{
"symbol": "ETHUSDT",
"direction": "buy",
"bot": "ta_trend",
"price": 2450.5,
"sl_pct": 3.0,
"tp_pct": 7.0,
"passed": 5,
"total": 7,
"status": "pending",
"tg_sent": true,
"age_seconds": 142
}
]
}
GET /api/signal/pending
Returns a formatted plain-text list of pending signals — ready to forward to the boss via COO.
Response (plain text)
📋 PENDING SIGNALS
━━━━━━━━━━━━━━━━━━
• ETHUSDT BUY | ta_trend
Price: 2450.5 | SL: 3.0% | TP: 7.0%
Score: 5/7 | Chờ: 2 phút
✅ "1-YES ETHUSDT" ❌ "0-NO ETHUSDT"
━━━━━━━━━━━━━━━━━━
GET /api/signal/{symbol}
Fetch full signal details for a specific symbol. Called by COO before spawning Finance.
Expiry guard: If the signal is older than 5 minutes the trade is blocked regardless of when the boss confirmed.
Example
curl "http://opentrader-bot:8000/api/signal/ETHUSDT"
Response (valid)
{
"symbol": "ETHUSDT",
"direction": "buy",
"bot": "ta_trend",
"price": 2450.5,
"sl_pct": 3.0,
"tp_pct": 7.0,
"passed": 5,
"total": 7,
"checklist": [{ "name": "RSI", "pass": true, "detail": "RSI=42" }],
"rsi": 42.1,
"macd_hist": 0.012,
"bb_lower": 2410.0,
"bb_upper": 2490.0,
"vol_ratio": 1.35,
"buy_signals": 5,
"sell_signals": 0,
"status": "pending",
"tg_sent": true
}
Error responses
| Code | Condition |
|---|---|
404 | No signal exists for this symbol |
410 | Signal exists but age > 5 min — boss confirmed too late, trade blocked |
409 | Signal exists but status is skipped or expired — cannot enter trade |
POST /api/signal/{symbol}/confirm
Called by Finance after a trade is successfully executed. The signal is removed from memory (consumed) — Scout can signal again for the same symbol once the position closes.
Example
curl -X POST "http://opentrader-bot:8000/api/signal/ETHUSDT/confirm"
Response
{ "ok": true, "symbol": "ETHUSDT", "status": "confirmed" }
Idempotent — calling multiple times does not error.
POST /api/signal/{symbol}/reject
Called by COO when the boss replies NO. Signal moves to skipped and remains in memory until cleanup runs.
Example
curl -X POST "http://opentrader-bot:8000/api/signal/ETHUSDT/reject"
Response
{ "ok": true, "symbol": "ETHUSDT", "status": "skipped" }
POST /api/signal/cleanup
Expire pending signals older than N minutes and delete stale entries. Called by Ops every 5 minutes.
Query params
| Param | Default | Description |
|---|---|---|
max_age_minutes | 5 | Pending signals older than this are marked expired |
Response
{
"ok": true,
"expired": ["ETHUSDT"],
"deleted": ["BTCUSDT"],
"count_expired": 1,
"count_deleted": 1
}
expired= pending → expired (age > N min).deleted= skipped/expired entries older than 30 min, removed from memory entirely.
Error codes
| Code | Meaning |
|---|---|
400 | Bad request / license validation error |
404 | Signal not found for symbol |
409 | Signal exists but status is not pending |
410 | Signal expired — boss confirmed after the 5-minute window |
503 | License required, or Telegram not configured |
504 | Bot subprocess timed out (> 300s) |
500 | Bot internal error or non-JSON output |
502 | Telegram API error |
Quick reference (curl)
BASE=http://opentrader-bot:8000
# Health
curl "$BASE/health"
# License
curl "$BASE/license/status"
curl -X POST "$BASE/license/register" \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","name":"Trader"}'
# Dashboard
curl "$BASE/api/state"
# News
curl "$BASE/api/news"
# Portfolio
curl "$BASE/api/portfolio"
# Bot actions
curl "$BASE/scan?bot=ta_trend"
curl "$BASE/status"
curl "$BASE/trailing"
curl "$BASE/custom_check?symbol=ETHUSDT&direction=buy&bot=ta_trend"
curl -X POST "$BASE/trade?symbol=ETHUSDT&direction=buy&bot=ta_trend&sl=3.0&tp=7.0"
curl -X POST "$BASE/api/closeall"
curl -X POST "$BASE/api/reset-daily"
# Telegram
curl -sG "$BASE/api/notify" --data-urlencode "message=Test thông báo"
# Trade management
curl "$BASE/api/trades"
curl "$BASE/api/trades/recover"
curl -X POST "$BASE/api/trades/restore" \
-H "Content-Type: application/json" \
-d '{"symbol":"BNBUSDT","direction":"buy","size":1.5,"entry_px":598.0,"sl_px":580.0,"tp_px":638.0,"sl_oid":10293001}'
# Signal flow — scan-and-signal (recommended)
curl -X POST "$BASE/api/scan-and-signal?bot=ta_trend&agent=scout"
# Signal flow — manual
curl -X POST "$BASE/api/signal" \
-H "Content-Type: application/json" \
-d '{"symbol":"ETHUSDT","direction":"buy","bot":"ta_trend","price":2450.5,"sl_pct":3.0,"tp_pct":7.0,"passed":5,"total":7}'
curl "$BASE/api/signal/list"
curl "$BASE/api/signal/pending"
curl "$BASE/api/signal/ETHUSDT"
curl -X POST "$BASE/api/signal/ETHUSDT/confirm"
curl -X POST "$BASE/api/signal/ETHUSDT/reject"
curl -X POST "$BASE/api/signal/cleanup?max_age_minutes=5"