Polymarket CLI 技術深度剖析:為什麼用 Rust 寫一個預測市場的命令列工具
2026 年 2 月底,Polymarket 開源了一個 Rust CLI 工具,讓使用者可以從終端機瀏覽預測市場、下單交易、管理鏈上資產。兩天內拿到 1,000+ stars。
為什麼一個預測市場平台要寫 CLI?為什麼用 Rust?這個專案的架構有什麼值得學習的地方?
本文從原始碼層面逐一拆解。
專案概覽
| 維度 | 數據 |
|---|---|
| 語言 | Rust (Edition 2024, MSRV 1.88.0) |
| 授權 | MIT |
| 原始碼行數 | ~8,700 行(不含 target) |
| 最大檔案 | output/clob.rs(1,467 行) |
| 依賴數 | 12 個直接依賴 |
| 測試 | 488 行整合測試 + 各模組單元測試 |
| 發布 | GitHub Releases + Homebrew + Shell 安裝腳本 |
| 跨平台 | macOS (x86/ARM), Linux (x86/ARM) |
核心是一個圍繞 polymarket-client-sdk 的 CLI 包裝層。SDK 提供 API 通訊和鏈上交互,CLI 負責命令解析、認證管理、輸出格式化。
架構設計
目錄結構
src/
main.rs -- 進入點,clap 命令解析,錯誤處理
auth.rs -- 錢包解析、RPC provider、CLOB 認證
config.rs -- 設定檔管理 (~/.config/polymarket/config.json)
shell.rs -- 互動式 REPL
commands/ -- 每個命令群組一個模組(14 個子模組)
output/ -- 每個命令群組的 table/JSON 渲染(14 個子模組)
這個目錄結構體現了一個清晰的關注點分離:
- commands/ 負責「做什麼」— 解析參數、呼叫 SDK、傳遞結果
- output/ 負責「怎麼顯示」— 將 SDK 回傳的資料結構渲染為 table 或 JSON
- auth.rs / config.rs 負責「誰在用」— 私鑰解析、認證狀態管理
命令分發:乾淨的 enum + match
main.rs 定義了一個 Commands enum,每個 variant 對應一個頂層命令:
#[derive(Subcommand)]
enum Commands {
Setup,
Shell,
Markets(commands::markets::MarketsArgs),
Events(commands::events::EventsArgs),
Clob(commands::clob::ClobArgs),
Ctf(commands::ctf::CtfArgs),
Data(commands::data::DataArgs),
// ... 共 16 個 variant
}
run() 函式用一個巨大的 match 將每個命令分發到對應的 commands::*::execute() 函式。這種模式在 Rust CLI 中非常常見 — 用型別系統保證所有命令都被處理,新增命令時編譯器會提醒你。
認證分層:讀取免費,交易要簽名
CLOB 模組(最核心的交易模組)內部再做了一層精細的分發:
pub async fn execute(args: ClobArgs, output: OutputFormat,
private_key: Option<&str>, signature_type: Option<&str>) -> Result<()> {
match args.command {
// 不需要錢包的讀取命令
ClobCommand::Ok | ClobCommand::Price { .. } | ClobCommand::Book { .. } | ...
=> execute_read(args.command, &output).await,
// 需要認證的交易命令
ClobCommand::CreateOrder { .. } | ClobCommand::Cancel { .. } | ...
=> execute_trade(args.command, &output, private_key, signature_type).await,
// 需要認證的獎勵查詢
ClobCommand::Rewards { .. } | ...
=> execute_rewards(args.command, &output, private_key, signature_type).await,
// 帳戶管理
ClobCommand::ApiKeys | ...
=> execute_account(args.command, &output, private_key, signature_type).await,
}
}
這個設計意味著:你不需要設定錢包就能瀏覽市場。polymarket markets list 和 polymarket clob book <token> 完全不需要私鑰。只有當你要下單、查餘額、管理 API key 時才需要認證。
這對 Agent 使用場景特別友好 — AI Agent 可以先無認證地探索所有市場數據,只在需要交易時才提供私鑰。
為什麼用 Rust
技術選型的考量
從 Cargo.toml 可以看到這個專案的依賴選擇:
[dependencies]
polymarket-client-sdk = { version = "0.4", features = ["gamma", "data", "bridge", "clob", "ctf"] }
alloy = { version = "1.6.3", features = ["providers", "sol-types", "contract", ...] }
clap = { version = "4", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tabled = "0.17"
serde_json = "1"
anyhow = "1"
關鍵依賴:
- alloy — Rust 生態中最主流的 Ethereum 工具庫(取代了 ethers-rs),處理鏈上交互、交易簽名
- clap — Rust 生態中的標準 CLI 框架,derive macro 自動生成參數解析
- tokio — 非同步運行時,處理所有 HTTP / RPC 呼叫
- tabled — 終端表格渲染
- polymarket-client-sdk — Polymarket 自己的 Rust SDK
Rust 的優勢
- 單一二進制,零依賴安裝
[profile.release]
lto = "thin"
codegen-units = 1
strip = true
panic = "abort"
這個 release profile 的每一行都在壓縮二進制大小:
lto = "thin"— 跨 crate 連結時間優化,消除未使用程式碼codegen-units = 1— 單一編譯單元,更好的全域優化strip = true— 去除 debug symbolpanic = "abort"— panic 時直接 abort 而非 unwind,省去 unwind 表
最終產物是一個自包含的二進制檔,curl | sh 就能安裝。不需要 Node.js runtime、不需要 Python 虛擬環境、不需要 Docker。
- 型別安全的命令解析
clap 的 derive macro 讓命令定義和型別檢查合為一體:
#[derive(Subcommand)]
pub enum ClobCommand {
CreateOrder {
#[arg(long)]
token: String,
#[arg(long)]
side: CliSide, // enum, 只接受 buy/sell
#[arg(long)]
price: String,
#[arg(long)]
size: String,
#[arg(long, default_value = "GTC")]
order_type: CliOrderType, // enum, 只接受 GTC/FOK/GTD/FAK
#[arg(long)]
post_only: bool,
},
// ...
}
所有參數在進入業務邏輯之前就已經被驗證過。CliSide 只能是 Buy 或 Sell,CliOrderType 只能是 GTC/FOK/GTD/FAK。這些是編譯時的保證,不是運行時的字串比對。
- 完整的 Ethereum 工具鏈
Rust 的 alloy 庫提供了完整的 Ethereum 原生操作能力:
pub async fn create_provider(private_key: Option<&str>) -> Result<impl Provider + Clone> {
let signer = LocalSigner::from_str(&key)?
.with_chain_id(Some(POLYGON));
ProviderBuilder::new()
.wallet(signer)
.connect(RPC_URL)
.await
}
直接在 Polygon 上建立 provider、簽名交易、呼叫合約。不需要透過 JavaScript bridge,不需要 Web3 wrapper。
- 跨平台編譯
release workflow 展示了 Rust 的交叉編譯能力:
matrix:
include:
- target: x86_64-apple-darwin
- target: aarch64-apple-darwin
- target: x86_64-unknown-linux-gnu
- target: aarch64-unknown-linux-gnu
cross: true
四個平台,一套程式碼。ARM Linux 使用 cross 工具做交叉編譯,其餘平台原生編譯。
Rust 的代價
- 編譯速度
12 個直接依賴看起來不多,但 alloy、tokio、polymarket-client-sdk 各自帶來大量傳遞依賴。從 Cargo.lock 的存在可以推斷完整依賴樹相當龐大。首次 cargo build 可能需要數分鐘。
- 字串處理的冗餘
CLI 工具不可避免地要大量處理字串。Rust 的所有權系統讓簡單的字串操作變得囉嗦:
fn normalize_key(key: &str) -> String {
if key.starts_with("0x") || key.starts_with("0X") {
key.to_string()
} else {
format!("0x{key}")
}
}
在 Python 或 Go 中,這是一行的事。在 Rust 中需要處理 &str vs String 的轉換。
- 非同步的傳染性
幾乎每個 execute() 函式都是 async fn,因為底層的 HTTP 呼叫都是非同步的。即使是簡單的 status 查詢也需要 tokio runtime。shell.rs 中的 Box::pin(shell::run_shell()).await 暗示了非同步遞迴的複雜性。
- 錯誤處理的模板程式碼
整個專案使用 anyhow::Result 做錯誤處理,幾乎每個可能失敗的操作都需要 .context("..."):
let signer = LocalSigner::from_str(&key)
.context("Invalid private key")?
.with_chain_id(Some(POLYGON));
ProviderBuilder::new()
.wallet(signer)
.connect(RPC_URL)
.await
.context("Failed to connect to Polygon RPC with wallet")
這在 Go 中是 if err != nil { return fmt.Errorf("...: %w", err) },冗餘程度差不多。但 Rust 的 ? 運算子至少比 Go 的三行式優雅一些。
安全模型
私鑰管理的三層優先級
/// Priority: CLI flag > env var > config file.
pub fn resolve_key(cli_flag: Option<&str>) -> (Option<String>, KeySource) {
if let Some(key) = cli_flag {
return (Some(key.to_string()), KeySource::Flag);
}
if let Ok(key) = std::env::var(ENV_VAR) && !key.is_empty() {
return (Some(key), KeySource::EnvVar);
}
if let Some(config) = load_config() {
return (Some(config.private_key), KeySource::ConfigFile);
}
(None, KeySource::None)
}
三種方式提供私鑰,優先級明確:
--private-keyflag — 一次性使用,不落磁碟POLYMARKET_PRIVATE_KEY環境變數 — 適合 CI/CD 和腳本~/.config/polymarket/config.json— 互動式使用
設定檔的權限保護
pub fn save_wallet(key: &str, chain_id: u64, signature_type: &str) -> Result<()> {
let dir = config_dir()?;
fs::create_dir_all(&dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&dir, fs::Permissions::from_mode(0o700))?;
}
// ...
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut file = fs::OpenOptions::new()
.write(true).create(true).truncate(true)
.mode(0o600)
.open(&path)?;
file.write_all(json.as_bytes())?;
}
}
在 Unix 系統上:
- 設定目錄權限設為
0o700(只有擁有者可讀寫執行) - 設定檔權限設為
0o600(只有擁有者可讀寫)
這是存放私鑰的基本安全措施。#[cfg(unix)] 條件編譯確保這些 Unix-specific 的權限設定不會在 Windows 上編譯失敗。
簽名類型
支援三種簽名方式:
- proxy(預設)— 使用 Polymarket 的代理錢包系統
- eoa — 直接用私鑰簽名
- gnosis-safe — 多簽錢包
代理錢包是 Polymarket 的特色設計 — 使用者的 EOA 授權一個代理地址來執行交易,降低了每次交易都需要直接暴露主私鑰的風險。
下載驗證
安裝腳本和自動更新都實作了 SHA-256 checksum 驗證:
fn verify_checksum(file_path: &str, checksums_file: &str, expected_name: &str) -> Result<()> {
// 下載 checksums.txt
// 比對 SHA-256
if actual_hash != expected_hash {
bail!("Checksum mismatch! The downloaded binary may have been tampered with.");
}
}
polymarket upgrade 命令在替換二進制時還做了原子性保護 — 先備份舊版本,新版本安裝成功後才刪除備份:
fs::rename(exe_path, &backup)?;
if let Err(e) = fs::rename(&new_binary, exe_path) {
let _ = fs::rename(&backup, exe_path); // 失敗時恢復
return Err(e);
}
let _ = fs::remove_file(&backup);
雙輸出格式:為人類和機器而設計
這可能是整個專案最值得學習的設計決策。
每個命令都支援兩種輸出格式:
# 人類友好的表格(預設)
polymarket markets list --limit 2
# 機器友好的 JSON
polymarket -o json markets list --limit 2
錯誤處理也遵循同樣的雙軌制:
if let Err(e) = run(cli).await {
match output {
OutputFormat::Json => {
println!("{}", serde_json::json!({"error": e.to_string()}));
}
OutputFormat::Table => {
eprintln!("Error: {e}");
}
}
return ExitCode::FAILURE;
}
- Table 模式:錯誤訊息寫入 stderr
- JSON 模式:錯誤以
{"error": "..."}格式寫入 stdout
無論哪種模式,失敗都回傳非零 exit code。
這個設計讓同一個工具可以同時服務兩種使用場景:
# 人類瀏覽
polymarket markets search "bitcoin"
# 腳本處理
polymarket -o json markets list --limit 100 | jq '.[].question'
# Agent 整合
result=$(polymarket -o json clob midpoint $TOKEN_ID)
output/ 目錄下 14 個模組、超過 3,000 行程式碼,全部在做同一件事:為每種資料結構提供 table 和 JSON 兩種渲染路徑。這是大量的重複工作,但也是讓 CLI 真正可用的關鍵。
互動式 Shell
pub async fn run_shell() {
let mut rl = rustyline::DefaultEditor::new()?;
loop {
match rl.readline("polymarket> ") {
Ok(line) => {
let args = split_args(line.trim());
let mut full_args = vec!["polymarket".to_string()];
full_args.extend(args);
match crate::Cli::try_parse_from(&full_args) {
Ok(cli) => {
if let Err(e) = crate::run(cli).await { /* ... */ }
}
Err(e) => { let _ = e.print(); }
}
}
// ...
}
}
}
shell 命令提供了一個互動式 REPL。實作非常巧妙 — 它不重新實作命令邏輯,而是把使用者輸入拼接成完整的 CLI 參數,然後用 Cli::try_parse_from() 重新走一遍 clap 的解析流程。
這意味著 shell 模式和 CLI 模式的行為保證完全一致 — 因為它們共用同一套解析和執行邏輯。
CTF 操作:鏈上合約交互
CTF (Conditional Token Framework) 模組展示了 CLI 如何直接和智能合約互動:
// 將 $10 USDC 拆分為 YES/NO 代幣
let req = SplitPositionRequest::builder()
.collateral_token(collateral_addr) // USDC 合約地址
.parent_collection_id(parent)
.condition_id(condition_id) // 市場的 condition ID
.partition(partition) // [1, 2] 代表二元結果
.amount(usdc_amount) // 10_000_000 (6 位小數)
.build();
let resp = client.split_position(&req).await?;
USDC 金額轉換有精確的精度檢查:
fn usdc_to_raw(val: Decimal) -> Result<U256> {
let raw = val * USDC_DECIMALS; // USDC_DECIMALS = 1_000_000
anyhow::ensure!(
raw.fract().is_zero(),
"Amount {val} exceeds USDC precision (max 6 decimal places)"
);
Ok(U256::from(raw_u64))
}
USDC 有 6 位小數精度。如果你輸入 0.0000001(7 位小數),CLI 會在本地就拒絕你,而不是把錯誤的數字送到鏈上。這種「提前失敗」的設計在金融應用中至關重要。
測試策略
整合測試
tests/cli_integration.rs 有 488 行、超過 30 個測試案例,覆蓋:
// 所有頂層命令都在 --help 中出現
fn help_lists_all_top_level_commands() { ... }
// 所有子命令群組的 --help 都列出子命令
fn markets_help_lists_subcommands() { ... }
fn clob_help_lists_subcommands() { ... }
// 缺少必要參數時正確失敗
fn markets_get_requires_id() { ... }
fn clob_book_requires_token() { ... }
// JSON 模式的錯誤輸出是有效的 JSON
fn json_mode_error_is_valid_json_with_error_key() { ... }
// Table 模式的錯誤走 stderr
fn table_mode_error_goes_to_stderr() { ... }
特別值得注意的是,測試會清除環境變數以避免汙染:
fn polymarket() -> Command {
let mut cmd = Command::cargo_bin("polymarket").unwrap();
cmd.env_remove("POLYMARKET_PRIVATE_KEY");
cmd.env_remove("POLYMARKET_SIGNATURE_TYPE");
cmd
}
單元測試
每個模組都有自己的 #[cfg(test)] mod tests,涵蓋邊界條件:
parse_usdc_amount("0.000001")— USDC 最小單位parse_usdc_amount("1.0000001")— 超出精度要拒絕truncate("cafe!", 3)— Unicode 字元計數而非位元組計數format_decimal(dec!(999_999))— 百萬邊界值
為什麼寫 CLI
這是最核心的問題,也是這個專案真正有趣的地方。
表面的答案:開發者工具
CLI 是開發者和 power user 的自然界面。你可以:
- 在終端快速查價
- 寫 shell 腳本做自動化交易
- 用
cron定期監控持倉 - 用
jq過濾和轉換數據
真正的答案:Agent Interface
這是原文引用點出的核心觀點:
CLIs are super exciting precisely because they are a "legacy" technology, which means AI agents can natively and easily use them, combine them, interact with them via the entire terminal toolkit.
CLI 是 Agent 的原生界面。原因:
- 無需瀏覽器自動化
GUI 需要 Selenium、Playwright 這類工具來模擬人類操作。CLI 只需要 subprocess.run()。
- 結構化輸入輸出
-o json 讓每個命令的輸出都是機器可解析的。Agent 不需要「理解」表格排版,直接解析 JSON。
- 可組合性
# Agent 可以自由組合這些命令
polymarket -o json markets search "AI" | \
jq -r '.[].slug' | \
xargs -I{} polymarket -o json markets get {} | \
jq '[.[] | {question, price: .outcomePrices[0]}]'
Unix 管道讓 CLI 天然具備可組合性。Agent 可以把多個命令串成 pipeline,不需要寫額外的膠水代碼。
- 零設定成本
安裝就是下載一個二進制檔。Agent 可以用 curl | sh 在幾秒內完成安裝。不需要瀏覽器 extension、不需要 OAuth flow、不需要 API key(大部分讀取操作)。
- 完整的操作覆蓋
這個 CLI 不是閹割版。它覆蓋了 Polymarket 的完整功能:
- 市場瀏覽與搜尋
- 訂單簿查詢
- 限價單 / 市價單交易
- 批量下單
- CTF 操作(拆分、合併、贖回)
- 跨鏈 Bridge
- 排行榜和數據查詢
Agent 用這個 CLI,能做到跟人類用 Web UI 一樣的所有事情。
2026 年的產品設計啟示
這個專案暗示了一個產品設計趨勢:
If you have any kind of product or service think: can agents access and use them?
核心問題不再只是「使用者體驗好不好」,而是「Agent 能不能用你的產品」。檢查清單:
- 文件是否有 Markdown 格式? Agent 能讀 Markdown,不能讀 Figma 設計稿
- 是否有 CLI 界面? CLI 是 Agent 的原生操作界面
- 是否有 MCP Server? MCP 是 AI Agent 的標準工具協議
- API 輸出是否結構化? JSON > HTML > 截圖
- 是否需要瀏覽器? 能不用就不用
Polymarket CLI 選擇了最直接的路線:一個輸出 JSON 的 CLI 工具。不需要 MCP adapter、不需要 API wrapper。任何能執行 shell 命令的 Agent 都能立即使用。
技術細節觀察
值得學習的
乾淨的 commands/output 分離
命令邏輯和渲染邏輯完全解耦。如果未來要加 CSV 輸出格式,只需要在 output/ 中加一個分支,不需要動 commands/ 的任何程式碼。
Builder pattern 的一致使用
SDK 的 request 物件全部使用 builder pattern:
let request = PriceHistoryRequest::builder()
.market(parse_token_id(&token_id)?)
.time_range(TimeRange::from_interval(Interval::from(interval)))
.maybe_fidelity(fidelity)
.build();
.maybe_*() 方法優雅地處理了 Option 類型的參數 — 有值就設定,None 就跳過。
Self-update 機制
polymarket upgrade 命令實作了完整的自我更新:查詢最新版本、下載、驗證 checksum、原子替換二進制。使用者不需要重新跑 brew install 或 curl | sh。
互動式 Shell 重用解析邏輯
Shell 模式不是另一套實作,而是把使用者輸入餵回 Cli::try_parse_from()。一份程式碼,兩種界面。
可以改進的
output/clob.rs 過大
1,467 行的單一檔案,包含超過 30 個 print 函式。每個函式的結構都是 match output { Table => ..., Json => ... }。可以用 macro 或 trait 減少重複。
沒有 --quiet / --verbose 層級
目前只有 table 和 JSON 兩種輸出模式。缺少靜默模式(只輸出 exit code)和詳細模式(加 debug 資訊)。
缺少 shell 模式的 tab 補全
rustyline 支援自定義補全器(completer),但目前的 shell 模式只有歷史記錄,沒有 tab 補全。加上命令補全會讓互動體驗更好。
小結
Polymarket CLI 是一個教科書級的 Rust CLI 專案:
- 用 Rust 寫 CLI 的理由充分 — 單一二進制、跨平台、Ethereum 生態的原生支援、型別安全的命令解析
- 架構設計清晰 — commands/output 分離、認證分層、雙輸出格式
- 安全意識到位 — 私鑰權限保護、下載 checksum 驗證、原子更新
- 真正的啟示在於 CLI 作為 Agent Interface — 這不是復古,而是前瞻
2026 年,你的產品能被 Agent 使用嗎?如果答案是否定的,那你可能已經落後了。一個好的 CLI + JSON 輸出,可能比花三個月做的 MCP Server 更實用。
Build. For. Agents.