Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A CLI, MCP server, and terminal dashboard for the [Crypto.com Exchange API](https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html). Single binary, zero runtime dependencies.

86 REST endpoints across 10 API groups, dynamically generated from the Crypto.com Exchange OpenAPI spec. Real-time WebSocket streaming. Full-screen TUI dashboard. Paper trading. Works as a standalone CLI, an MCP tool server for AI agents, or an interactive terminal.
95 REST endpoints across 11 API groups, dynamically generated from the Crypto.com Exchange OpenAPI spec. Real-time WebSocket streaming. Full-screen TUI dashboard. Paper trading. Works as a standalone CLI, an MCP tool server for AI agents, or an interactive terminal.

> **Caution:** This software interacts with the live Crypto.com Exchange and can execute real financial transactions. Test with `cdcx paper` before using real funds.

Expand All @@ -24,7 +24,7 @@ cargo install --git https://github.com/crypto-com/cdcx-cli.git --bin cdcx

### For AI Agents

Every response is structured JSON. 86 MCP tools with typed parameters, enum validation, safety enforcement, and schema discovery — all generated from the OpenAPI spec at runtime. Your LLM can trade, analyze markets, and manage positions without custom tooling.
Every response is structured JSON. 95 MCP tools with typed parameters, enum validation, safety enforcement, and schema discovery — all generated from the OpenAPI spec at runtime. Your LLM can trade, analyze markets, manage positions, and orchestrate trading bots without custom tooling.

```bash
cdcx mcp config --enable trade,account
Expand Down Expand Up @@ -104,7 +104,7 @@ cdcx mcp config --allow-dangerous # Allow withdrawals, cancel-all
cdcx mcp config --reset # Reset to defaults (market only)
```

Service groups: `market`, `account`, `trade`, `advanced`, `margin`, `staking`, `funding`, `fiat`, `otc`, `stream`
Service groups: `market`, `account`, `trade`, `advanced`, `margin`, `staking`, `funding`, `fiat`, `otc`, `bot`, `stream`

Configuration is stored in `~/.config/cdcx/mcp.toml` and persists across updates.

Expand All @@ -115,9 +115,9 @@ Configuration is stored in `~/.config/cdcx/mcp.toml` and persists across updates
| Tier | Behavior | Examples |
|------|----------|---------|
| **read** | No confirmation | `market ticker`, `market book` |
| **sensitive_read** | No confirmation | `account summary`, `trade open-orders` |
| **mutate** | Requires `acknowledged: true` | `trade order`, `trade cancel` |
| **dangerous** | Requires `--allow-dangerous` | `trade cancel-all`, `wallet withdraw` |
| **sensitive_read** | No confirmation | `account summary`, `bot list` |
| **mutate** | Requires `acknowledged: true` | `trade order`, `bot create`, `bot pause` |
| **dangerous** | Requires `--allow-dangerous` | `trade cancel-all`, `bot terminate`, `wallet withdraw` |

### Plugin Installation

Expand Down Expand Up @@ -245,6 +245,20 @@ cdcx advanced create-otoco --instrument-name BTC_USDT ...
cdcx advanced open-orders
```

### Trading Bots

```bash
cdcx bot create DCA --notify-on-bot-change true --json '{"settings": {"is_basket": false, "allocations": [{"instrument_name": "BTC_USDT", "allocated_percentage": "100"}], "investment_currency": "USDT", "side": "BUY", "type": "MARKET", "quantity": "0.01", "investment_frequency_mode": "FIXED_DURATION", "investment_frequency": 300000, "max_order": "10"}}'
cdcx bot list --bot-types DCA --state RUNNING
cdcx bot pause 123 --bot-type DCA
cdcx bot resume 123 --bot-type DCA
cdcx bot update 123 --bot-type GRID --json '{"settings": {"stop_price": "50000"}}'
cdcx bot executions 123
cdcx bot terminate 123 --bot-type DCA
```

Bot types: `DCA`, `TWAP`, `GRID`, `FUNDING_ARBITRAGE`. Complex settings are passed via `--json`.

### Paper Trading

Local paper trading engine with live market prices. No auth required.
Expand Down
1 change: 1 addition & 0 deletions agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ cdcx mcp config --reset # Reset to defaults (market only)
| `funding` | wallet | No | dangerous | Withdrawals and fund transfers |
| `fiat` | fiat | No | dangerous | Fiat deposits/withdrawals |
| `otc` | otc | No | mutate | OTC desk operations |
| `bot` | bot | No | mutate/dangerous | Trading bot management (DCA, TWAP, GRID, FUNDING_ARBITRAGE) |
| `stream` | stream | No | mixed | WebSocket subscriptions |

### Example Configurations
Expand Down
5 changes: 5 additions & 0 deletions crates/cdcx-cli/src/cli_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ pub fn extract_params(matches: &clap::ArgMatches, params: &[ParamSchema]) -> ser
serde_json::json!(value_str)
}
}
"boolean" => match value_str.as_str() {
"true" | "1" => serde_json::json!(true),
"false" | "0" => serde_json::json!(false),
_ => serde_json::json!(value_str),
},
"json" => {
serde_json::from_str(value_str).unwrap_or_else(|_| serde_json::json!(value_str))
}
Expand Down
49 changes: 49 additions & 0 deletions crates/cdcx-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,55 @@ pub async fn dispatch_dynamic(
// Extract params from ArgMatches using schema types
let mut params = cli_builder::extract_params(cmd_matches, &endpoint.params);

// Validate required params when --json is not provided (--json may supply them)
if global.json_input.is_none() {
let obj = params.as_object().cloned().unwrap_or_default();
let missing: Vec<String> = endpoint
.params
.iter()
.filter(|p| p.required && !obj.contains_key(&p.name))
.map(|p| {
if p.position.is_some() {
format!("<{}>", p.name)
} else {
format!("--{} <{}>", p.name.replace('_', "-"), p.name)
}
})
.collect();
if !missing.is_empty() {
let usage_args: String = endpoint
.params
.iter()
.filter(|p| p.required)
.map(|p| {
if p.position.is_some() {
format!("<{}>", p.name)
} else {
format!("--{} <{}>", p.name.replace('_', "-"), p.name)
}
})
.collect::<Vec<_>>()
.join(" ");
let cmd_name: &'static str =
Box::leak(format!("cdcx {} {}", group, command).into_boxed_str());
let usage: &'static str =
Box::leak(format!("cdcx {} {} {}", group, command, usage_args).into_boxed_str());
let mut cmd = clap::Command::new(cmd_name).override_usage(usage);
let err = cmd.error(
clap::error::ErrorKind::MissingRequiredArgument,
format!(
"the following required arguments were not provided:\n{}",
missing
.iter()
.map(|m| format!(" {}", m))
.collect::<Vec<_>>()
.join("\n")
),
);
err.exit();
}
}

// Stamp client_oid with the cx1- CLI origin prefix so orders placed via `cdcx`
// are identifiable downstream. Both create-order and advanced/create-order use
// a scalar client_oid; create-order-list puts client_oid on each leg.
Expand Down
32 changes: 31 additions & 1 deletion crates/cdcx-cli/src/mcp/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ pub fn mcp_to_schema_groups(mcp_group: &str) -> Vec<&'static str> {
"fiat" => vec!["fiat"],
"stream" => vec!["stream"], // stream tools aren't in schema yet
"otc" => vec!["otc"],
"bot" => vec!["bot"],
"all" => vec![
"market", "account", "history", "trade", "advanced", "wallet", "fiat", "staking",
"margin", "otc",
"margin", "otc", "bot",
],
_ => vec![],
}
Expand Down Expand Up @@ -159,6 +160,7 @@ mod tests {
assert_eq!(mcp_to_schema_groups("account"), vec!["account", "history"]);
assert_eq!(mcp_to_schema_groups("funding"), vec!["wallet"]);
assert_eq!(mcp_to_schema_groups("fiat"), vec!["fiat"]);
assert_eq!(mcp_to_schema_groups("bot"), vec!["bot"]);
assert!(mcp_to_schema_groups("unknown").is_empty());
}

Expand Down Expand Up @@ -210,6 +212,7 @@ mod tests {
assert!(tools.iter().any(|t| t.name.starts_with("cdcx_account_")));
assert!(tools.iter().any(|t| t.name.starts_with("cdcx_funding_")));
assert!(tools.iter().any(|t| t.name.starts_with("cdcx_fiat_")));
assert!(tools.iter().any(|t| t.name.starts_with("cdcx_bot_")));

// Should have tools from all fixture groups
assert!(
Expand All @@ -219,6 +222,33 @@ mod tests {
);
}

#[test]
fn test_tool_generation_bot_group() {
let registry =
SchemaRegistry::from_fixture_with_overlays().expect("Failed to parse fixture");
let tools = generate_tools(&registry, &["bot".to_string()]);

assert!(tools.iter().any(|t| t.name == "cdcx_bot_create"));
assert!(tools.iter().any(|t| t.name == "cdcx_bot_terminate"));
assert!(tools.iter().any(|t| t.name == "cdcx_bot_list"));
assert!(tools.iter().any(|t| t.name == "cdcx_bot_executions"));

// terminate is dangerous — should have acknowledged param
let terminate_tool = tools
.iter()
.find(|t| t.name == "cdcx_bot_terminate")
.unwrap();
let schema_obj = terminate_tool.input_schema.as_ref();
let properties = schema_obj
.get("properties")
.and_then(|p| p.as_object())
.expect("Should have properties");
assert!(
properties.contains_key("acknowledged"),
"Dangerous tool should have acknowledged parameter"
);
}

#[test]
fn test_tool_generation_filters_groups() {
let registry = test_registry();
Expand Down
1 change: 1 addition & 0 deletions crates/cdcx-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ pub const MCP_SERVICE_GROUPS: &[(&str, &str)] = &[
("funding", "Withdrawals (dangerous)"),
("fiat", "Fiat operations (dangerous)"),
("otc", "OTC desk operations"),
("bot", "Trading bot management"),
("stream", "Real-time data streams"),
];

Expand Down
59 changes: 57 additions & 2 deletions crates/cdcx-core/src/openapi/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub fn tag_to_group(tag: &str) -> Option<&'static str> {
"Staking" => Some("staking"),
"Transaction History" => Some("history"),
"OTC RFQ for Taker" => Some("otc"),
"Trading Bot API" => Some("bot"),
_ => None,
}
}
Expand All @@ -57,6 +58,7 @@ pub fn group_description_for(group: &str) -> &'static str {
"staking" => "Staking endpoints",
"history" => "Transaction history endpoints",
"otc" => "OTC RFQ trading endpoints",
"bot" => "Automated trading bot management",
_ => "API endpoints",
}
}
Expand Down Expand Up @@ -151,6 +153,13 @@ pub fn derive_command_name(method: &str, group: &str) -> String {
name = rest.to_string();
}
}
"bot" => {
if let Some(rest) = name.strip_prefix("trading-bot-") {
name = rest.to_string();
} else if let Some(rest) = name.strip_suffix("-trading-bot") {
name = rest.to_string();
}
}
_ => {}
}

Expand All @@ -160,7 +169,10 @@ pub fn derive_command_name(method: &str, group: &str) -> String {
/// Derives a safety tier from the method path.
pub fn derive_safety_tier(method: &str) -> &'static str {
// Dangerous operations
if method == "private/create-withdrawal" || method == "private/fiat/fiat-create-withdraw" {
if method == "private/create-withdrawal"
|| method == "private/fiat/fiat-create-withdraw"
|| method == "private/bot/terminate-trading-bot"
{
return "dangerous";
}

Expand Down Expand Up @@ -649,7 +661,14 @@ fn extract_parameters(
.get("description")
.and_then(|d| d.as_str())
.unwrap_or_default();
let raw_type = val.get("type").and_then(|t| t.as_str()).unwrap_or("string");
let raw_type =
val.get("type").and_then(|t| t.as_str()).unwrap_or_else(|| {
if val.get("oneOf").is_some() || val.get("anyOf").is_some() {
"object"
} else {
"string"
}
});
let mut is_required = required_list.contains(&name.to_string());

let enum_values = extract_enum_values(val, openapi_doc, schema_doc);
Expand Down Expand Up @@ -802,6 +821,7 @@ mod tests {
assert_eq!(tag_to_group("OTC RFQ for Taker"), Some("otc"));
assert_eq!(tag_to_group("Staking"), Some("staking"));
assert_eq!(tag_to_group("Fiat Wallet"), Some("fiat"));
assert_eq!(tag_to_group("Trading Bot API"), Some("bot"));
}

#[test]
Expand Down Expand Up @@ -1139,6 +1159,28 @@ mod tests {
derive_command_name("private/get-transactions", "history"),
"transactions"
);

// Bot
assert_eq!(
derive_command_name("private/bot/terminate-trading-bot", "bot"),
"terminate"
);
assert_eq!(
derive_command_name("private/bot/update-trading-bot", "bot"),
"update"
);
assert_eq!(
derive_command_name("private/bot/pause-trading-bot", "bot"),
"pause"
);
assert_eq!(
derive_command_name("private/bot/resume-trading-bot", "bot"),
"resume"
);
assert_eq!(
derive_command_name("private/bot/get-trading-bot-executions", "bot"),
"executions"
);
}

#[test]
Expand All @@ -1157,6 +1199,15 @@ mod tests {
"mutate"
);
assert_eq!(derive_safety_tier("private/user-balance"), "read");
assert_eq!(
derive_safety_tier("private/bot/terminate-trading-bot"),
"dangerous"
);
assert_eq!(
derive_safety_tier("private/bot/create-trading-bot"),
"mutate"
);
assert_eq!(derive_safety_tier("private/bot/get-trading-bots"), "read");
}

#[test]
Expand All @@ -1166,6 +1217,10 @@ mod tests {
"Public market data endpoints"
);
assert_eq!(group_description_for("otc"), "OTC RFQ trading endpoints");
assert_eq!(
group_description_for("bot"),
"Automated trading bot management"
);
assert_eq!(group_description_for("unknown"), "API endpoints");
}

Expand Down
38 changes: 36 additions & 2 deletions crates/cdcx-core/src/safety.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ impl SafetyTier {
"private/cancel-all-orders"
| "private/advanced/cancel-all-orders"
| "private/create-withdrawal"
| "private/fiat/fiat-create-withdraw" => Self::Dangerous,
| "private/fiat/fiat-create-withdraw"
| "private/bot/terminate-trading-bot" => Self::Dangerous,

// Mutate operations
"private/create-order"
Expand All @@ -50,7 +51,11 @@ impl SafetyTier {
| "private/staking/stake"
| "private/staking/unstake"
| "private/staking/convert"
| "private/create-subaccount-transfer" => Self::Mutate,
| "private/create-subaccount-transfer"
| "private/bot/create-trading-bot"
| "private/bot/update-trading-bot"
| "private/bot/pause-trading-bot"
| "private/bot/resume-trading-bot" => Self::Mutate,

// Everything else that's private is SensitiveRead
_ => Self::SensitiveRead,
Expand Down Expand Up @@ -150,6 +155,35 @@ mod tests {
SafetyTier::from_method("private/create-withdrawal"),
SafetyTier::Dangerous
);
// Bot endpoints
assert_eq!(
SafetyTier::from_method("private/bot/create-trading-bot"),
SafetyTier::Mutate
);
assert_eq!(
SafetyTier::from_method("private/bot/update-trading-bot"),
SafetyTier::Mutate
);
assert_eq!(
SafetyTier::from_method("private/bot/pause-trading-bot"),
SafetyTier::Mutate
);
assert_eq!(
SafetyTier::from_method("private/bot/resume-trading-bot"),
SafetyTier::Mutate
);
assert_eq!(
SafetyTier::from_method("private/bot/terminate-trading-bot"),
SafetyTier::Dangerous
);
assert_eq!(
SafetyTier::from_method("private/bot/get-trading-bots"),
SafetyTier::SensitiveRead
);
assert_eq!(
SafetyTier::from_method("private/bot/get-trading-bot-executions"),
SafetyTier::SensitiveRead
);
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions crates/cdcx-core/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ impl SchemaRegistry {
include_str!("../../../schemas/apis/staking.toml"),
include_str!("../../../schemas/apis/margin.toml"),
include_str!("../../../schemas/apis/history.toml"),
include_str!("../../../schemas/apis/bot.toml"),
]
.iter()
.map(|s| {
Expand Down
Loading