mcpField
M
MagicMarkets

MagicMarkets

远程服务
magicmarkets.com游戏娱乐1今天更新

关于 MagicMarkets

MagicMarkets 是面向 Magic Markets v2 API 的命令行工具,供使用体育预测市场的用户和 Agent 使用。它可订阅价格、管理报价与订单,并将相同 API 能力作为 MCP 工具提供。

主要功能

  1. 01订阅实时体育价格和账户动态
  2. 02查找赛事及有报价的选项
  3. 03创建 betslip 并下单
  4. 04查看持仓、余额和订单
  5. 05管理到期后关闭订单的 heartbeat

适用场景

  1. 01监控指定赛事的实时价格
  2. 02为选项询价并提交订单
  3. 03使用 heartbeat 保护自动化策略

MCP 相关

命令行工具通过 stdio 或本地主机上的 streamable HTTP 提供 MCP 工具。除非设置 `MAGICMARKETS_ALLOW_TRADING`,否则 MCP 交易能力处于禁用状态。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://magicmarkets.com/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

工具

详细介绍

magicmarkets-cli

用于调用 Magic Markets v2 API 的命令行界面:实时获取体育赛事价格、为选项报价、下单和管理订单,并查看持仓。同一 API 也可作为 MCP 工具使用,可通过 stdio(客户端以子进程方式启动 magicmarkets mcp),也可通过 localhost 上的流式 HTTP 传输使用。

单个静态二进制文件,只需一个 API 密钥即可进行身份验证。无需请求签名,也无需私钥。

magicmarkets markets --sport fb                     # find events with live prices
magicmarkets offers fb 2026-06-15,1001,2002         # list priced bet types
magicmarkets betslip create fb 2026-06-15,1001,2002 for,h --wait 5s
magicmarkets order place --betslip bs-123 --price 2.10 --stake 50
  • 设置 — 安装、身份验证、首批命令
  • 使用 — 下单流程、命令参考、MCP、价格、错误
  • 开发 — 项目结构、代码生成、约定、贡献

设置

1. 安装

从克隆的仓库安装:

git clone https://github.com/magicmarkets/magicmarkets-cli
cd magicmarkets-cli
make install                 # installs `magicmarkets` into your Go bin directory

make where 会准确打印安装位置。如果之后找不到 magicmarkets,说明该目录不在你的 PATH 中:

export PATH="$PATH:$(go env GOPATH)/bin"      # add to ~/.zshrc or ~/.bashrc

不想安装?make build 会生成 ./build/magicmarkets,且不会更改你的 PATH。

go install 直接安装

注意 /cmd/magicmarkets 路径 — 安装模块根目录会生成名为 magicmarkets-cli 的二进制文件:

go install ./cmd/magicmarkets

Go 模块路径是 magicmarkets-cli,而不是 GitHub URL,因此 go install github.com/…/magicmarkets-cli@latest 无法使用。支持的方式是克隆仓库并运行 make install。

2. 添加 API 密钥

在 magicmarkets.com 的 设置 → API 中创建密钥。密钥创建时只显示一次,请立即妥善保存。

将其放入 ~/.magicmarkets/.env,以便从任意目录使用:

mkdir -p ~/.magicmarkets
echo 'MAGICMARKETS_API_KEY=your-key-here' > ~/.magicmarkets/.env
chmod 600 ~/.magicmarkets/.env

也可以使用环境变量或项目本地的 .env — 运行 cp env.example .env 并填写密钥即可。

3. 验证

$ magicmarkets status
version        v1.0.0
api url        https://magicmarkets.com/v2
ws url         wss://magicmarkets.com/v2/stream
lang           en
api key        ***********1234
env files      [/Users/you/.magicmarkets/.env]
authenticated  yes

打开数据流之前务必先检查这一项 — WebSocket 会在握手时拒绝无效密钥,且不会提供有用的错误信息。

设置到此完成。以下内容均为可选。

试用

magicmarkets balance                          # money position
magicmarkets xrates                           # exchange rates to USDT
magicmarkets markets --sport fb --limit 5     # events that currently have prices
magicmarkets offers fb <event-id> --depth 2   # priced bet types on one event
magicmarkets orders --open                    # your open orders
magicmarkets ticks 2.345                      # where a price lands on the tick schedule
magicmarkets api endpoints                    # every endpoint (no key, no network)

在任意命令后添加 --json,即可将输出传递给 jq。

配置

按以下顺序解析,采用第一个匹配项:

优先级来源
1实际环境变量
2./.env
3~/.magicmarkets/.env
4~/.env
5内置默认值
变量默认值用途
MAGICMARKETS_API_KEY—API 密钥(X-Api-Key)。除非设置了 MAGICMARKETS_ACCESS_TOKEN,否则为必填项
MAGICMARKETS_ACCESS_TOKEN—用于 CLI/stdio 的 OAuth Bearer 令牌。HTTP MCP 仍会在每个请求中获取令牌
MAGICMARKETS_OAUTH_ISSUERhttps://magicmarkets.com/api/auth上游 AS,mcp --http 会将 /authorize 和 /token 代理到该地址;POST /oauth2/firebase-token 也会基于该地址解析 Bearer 凭据 — 参见身份验证
MAGICMARKETS_SESSION_GROUP_ID—Magic Markets 提供的环境专属 ID,用于构建 OAuth Bearer 凭据所解析到的 session 值。没有安全的默认值 — 任何需要 Bearer 凭据的场景都必须设置
MAGICMARKETS_FIREBASE_WEB_API_KEY—Magic Markets Firebase 项目的 Firebase Web API 密钥,用于将 /oauth2/firebase-token 返回的 Firebase 自定义令牌交换为真实的 Firebase ID 令牌 — 参见身份验证。没有安全的默认值 — 任何需要 Bearer 凭据的场景都必须设置
MAGICMARKETS_OAUTH_CLIENT_ID—预先注册的客户端,用于允许列表放行此主机自己的 /mcp/callback
MAGICMARKETS_OAUTH_PROXY_SECRET—用于密封 OAuth 代理状态;所有副本必须共用此密钥
MAGICMARKETS_MCP_PUBLIC_URL—mcp --http 的公共基址(此主机是 MCP 授权服务器)
MAGICMARKETS_API_URLhttps://magicmarkets.com/v2REST 基址,包含 /v2
MAGICMARKETS_BASIC_AUTH—Base64 编码的 user:pass,作为额外的 Authorization: Basic 标头随每个 /v2/* REST 调用发送 — 用于通过某些非生产环境在 v2 API 前设置的基础设施级访问墙。生产环境没有此类访问墙,因此不会设置该变量。绝不会将其发送到 /oauth2/firebase-token、Firebase 的 signInWithCustomToken 或 WebSocket 流
MAGICMARKETS_WS_URL根据 MAGICMARKETS_API_URL 派生流端点
MAGICMARKETS_LANGen事件名称语言:en、ko、zh-hans
MAGICMARKETS_TIMEOUT30s单次请求超时时间
MAGICMARKETS_ALLOW_TRADING未设置(关闭)允许 magicmarkets mcp 下单。对 CLI 无效。

全局标志:--json、--verbose/-v、--api-key、--api-url、--ws-url。

除非通过 MAGICMARKETS_WS_URL 或 --ws-url 显式固定流端点,否则 --api-url 会根据自身重新派生流端点(方式与 MAGICMARKETS_WS_URL 的派生方式一致)——因此,使用 --api-url https://staging... 时,不会出现 stream 仍读取生产价格、而其他所有命令都访问暂存环境的情况。


使用方法

两步下注流程

下注总是分两步进行:

  1. 投注单:登记对某个选项的关注,并接收实时报价。无需付费,也不会提交任何内容。
  2. 订单:根据该报价提交投注金额。

投注单的有效期很短,创建时没有价格 — 报价会通过 WebSocket 以 pmm 消息的形式异步到达,或者通过轮询获取。因此需要使用 --wait:

# 1. find an event that has prices
$ magicmarkets markets --sport fb --limit 5
SPORT  EVENT ID              EVENT               COMPETITION             STATUS     START
-----  --------              -----               -----------             ------     -----
fb     2026-06-15,1001,2002  Arsenal v Chelsea   England Premier League  pre_event  2026-06-15 16:00:00

# 2. read a bet_type straight off the feed
$ magicmarkets offers fb 2026-06-15,1001,2002 --depth 2
BET TYPE          MARKET  IR  PRICES (stake @ price)      TOTAL
--------          ------  --  ----------------------      -----
for,h             1x2     -   [email protected]  [email protected]     230.00
for,ah,h,-4       ah      -   [email protected]  [email protected]    320.00

# 3. quote it, waiting for the price to land
$ magicmarkets betslip create fb 2026-06-15,1001,2002 for,h --wait 5s
betslip id       bs-abc123
bet type         for,h
description      Home
expires          2026-06-15 15:42:10 (28s)
total available  230.00 USDT

Prices:
PRICE  MIN   MAX
-----  ---   ---
2.10   5.00  150.00
2.08   -     80.00

# 4. commit a stake (asks for confirmation)
$ magicmarkets order place --betslip bs-abc123 --price 2.10 --stake 50

切勿手动构造 bet_type。 请从 magicmarkets offers 或流中逐字复制该值 — 它用一个字符串编码了市场、让分、结果和方向。

命令参考

账户
命令用途
magicmarkets status显示配置并验证密钥
magicmarkets balance余额、未结投注、智能额度、可用余额
magicmarkets xrates兑 USDT 汇率
magicmarkets position汇总盈亏;使用 --grid 查看收益矩阵
发现
命令用途
magicmarkets markets当前有价格的赛事
magicmarkets offers <sport> <event-id>某场赛事中已定价的投注类型
magicmarkets stream持续读取实时价格和账户信息流

v2 REST API 没有赛事列表端点——赛事发现通过 WebSocket 完成。这些命令会连接、读取快照,然后断开,因此需要几秒钟。

magicmarkets markets --sport fb,tennis --limit 20
magicmarkets markets --search arsenal
magicmarkets markets --in-play
magicmarkets offers fb 2026-06-15,1001,2002 --market ah --depth 3
magicmarkets stream --register fb:2026-06-15,1001,2002
magicmarkets stream --type order,bet          # only order activity
交易
命令用途
magicmarkets betslip create [sport] [event] [bet-type]为一个选项报价
magicmarkets betslip get <id>显示投注单及其价格
magicmarkets betslip list列出未关闭的投注单 ID(使用 --expand 查看完整详情)
magicmarkets betslip refresh <id>延长有效期
magicmarkets order place根据投注单下单
magicmarkets orders / magicmarkets order list列出订单
magicmarkets order get <id>显示一笔订单及其投注
magicmarkets order tracked <uuid>通过幂等键查找订单
magicmarkets order updates查看指定时间范围内发生变更的订单
magicmarkets order close <id>取消一笔订单
magicmarkets order close-many <id>...取消最多 500 笔订单
magicmarkets order close-all取消所有未关闭的订单

Lay 投注和串关:

# lay (against) a selection
magicmarkets betslip create --lay fb 2026-06-15,1001,2002 for,over,2.5

# a 2-leg accumulator
magicmarkets betslip create \
  --leg fb:2026-06-15,1001,2002:for,h \
  --leg fb:2026-06-16,1003,2004:for,over,2.5 --wait 5s
风险控制
命令用途
magicmarkets heartbeat run创建心跳并在前台保持运行
magicmarkets heartbeat create/list/get/refresh/cancel直接管理心跳

心跳是一种失效保护开关:如果在到期前未刷新,所有未关闭的订单都会自动关闭。运行自动化策略时,应同时运行心跳,以免进程崩溃后订单仍处于开放状态。

$ magicmarkets heartbeat run --timeout 60
heartbeat hb-xyz created, expires 2026-06-15 15:43:10; refreshing every 20s
press Ctrl-C to cancel it and leave orders open

按下 Ctrl-C 时,心跳会正常取消,订单保持开放。如果进程意外终止,失效保护开关就会触发。

MCP
命令用途
magicmarkets mcp默认通过 stdio 提供 MCP 工具,也可使用 --http 在 localhost 上启用可流式传输的 HTTP 传输。参见 MCP
参考
命令用途
magicmarkets bet-type <sport> <bet-type>验证投注类型并显示其收益网格
magicmarkets ticks <price>将价格对齐到价格档位表
magicmarkets api endpoints列出所有端点
magicmarkets api show <path> [method]查看端点详情:参数、请求体、响应
magicmarkets api schema [name]查看组件架构
magicmarkets api curl <method> <path>生成可运行的 curl 命令
magicmarkets api search <term>搜索端点和架构
magicmarkets api spec打印内嵌的 OpenAPI 规范

magicmarkets api 命令无需 API 密钥,也无需网络——OpenAPI 3.1 规范已编译进二进制文件。

安全操作清单

在运行任何会花钱的操作前,建议牢记以下事项:

  • 每笔订单都传入 --request-uuid。 这可以确保下单操作具备幂等性:超时后重试不会创建第二笔订单,而且订单在 6 小时内仍可查询。若不传此参数,超时后你就无法确定投注是否已下单。
  • 自动化运行时启用心跳。 若未启用,策略进程崩溃后,订单仍会留在市场中。
  • 打开数据流之前,先通过 REST 验证密钥。 WebSocket 握手失败时不会返回有用的错误信息。
  • 从数据流中获取 bet_type,不要手动填写。 亚洲让分盘的盘口线是实际盘口线的 4 倍,因此手动构造字符串很容易出错,且可能不会提示。
  • 检查对齐后的价格。 magicmarkets order place 会在确认信息中显示该价格;订单实际执行时使用的是这个价格,而不是你输入的价格。

价格与跳动档位

每个价格都遵循固定的跳动档位,价格越高,档位间距越大:

十进制价格跳动档位
1.01 – 20.01
2 – 30.02
3 – 40.05
4 – 60.10
6 – 100.20
10 – 200.50
20 – 301
30 – 502
50 – 1005
100 – 100010

不符合跳动档位的订单价格会被舍入,确保不会让你的限价更不利:back(for)订单向下舍入,lay(against)订单向上舍入。magicmarkets order place 会自动调整价格,并在确认信息中显示结果。

$ magicmarkets ticks 2.345
snapped price    2.34        # back: rounded down
$ magicmarkets ticks 2.345 --lay
snapped price    2.36        # lay: rounded up

从行情源获取的报价已经符合跳动档位,不会再次舍入。

下注类型语法

bet_type 是以方向开头的逗号分隔字符串:for 表示 back,against 表示 lay。让球数始终指主队。

示例含义
for,h / for,d / for,a主胜 / 平局 / 客胜
for,dnb,h主胜,若平局则作废
for,dc,h,d双重机会:主胜或平局
for,over,2.5 / for,under,2.5大于 / 小于 2.5 球
for,ah,h,-4亚洲让球盘,主队 -1.0
for,ahover,7亚洲总进球数大于 1.75
for,cs,2,1正确比分 2–1
for,score,both,yes两队均得分
for,win,<team_id>选手赢得一场独赢市场
for,top,3,<team_id>选手进入前三名

亚洲让球盘的盘口线是整数,等于实际盘口线的 4 倍 — -4 表示 -1.0,2 表示 +0.5,7 表示 +1.75。这样,间隔为 0.25 的盘口线在传输格式中也只需使用整数。

验证候选字符串:

$ magicmarkets bet-type fb for,ah,h,-4
description  Home -1.0 (Asian)
valid        yes

完整语法 — 包括网球局数、时间段标记和所有市场 — 见 docs/api-reference.md。

JSON 输出

每个命令都支持 --json:

magicmarkets orders --open --json | jq -r '.[] | "\(.order_id) \(.status)"'
magicmarkets markets --sport fb --json | jq -r '.[].event_id'
magicmarkets stream --type order --json          # one JSON object per line

投注金额是由两个元素组成的元组,而不是对象 — --json 会原样映射 API 传输格式,因此应按索引访问,而不是查找字段名:

$ magicmarkets balance --json
{
  "balance": ["USDT", 10000.5],
  "open_stake": ["USDT", 152.55],
  "smart_credit": null
}

$ magicmarkets balance --json | jq '.balance[1]'      # amount
10000.5
$ magicmarkets balance --json | jq -r '.balance[0]'   # currency
USDT

所有金额字段也都适用:want_stake、stake、profit_loss、total,以及价格档位中的 min / max。

MCP

magicmarkets mcp 会将 API 作为 MCP 工具提供,因此大模型 Agent 可以读取价格并管理订单。默认使用 stdio:MCP 客户端(如 Claude Code、Cursor)会将其作为子进程启动,双方通过 stdin/stdout 交换 JSON-RPC。传入 --http 则改为在 localhost 上提供 MCP 可流式传输的 HTTP 传输 — 详见下文的通过 localhost HTTP 提供服务。

stdio

向客户端注册此命令:

claude mcp add magicmarkets -e MAGICMARKETS_API_KEY=your-key -- magicmarkets mcp

或者在客户端的 MCP 配置中配置 — mcpServers 是客户端对 stdio 子进程的称呼,并非网络服务:

{
  "mcpServers": {
    "magicmarkets": {
      "command": "magicmarkets",
      "args": ["mcp"],
      "env": { "MAGICMARKETS_API_KEY": "your-key" }
    }
  }
}

目前 MCP 支持仍处于开发者模式 — 将 stdio 服务接入特定客户端时可能会遇到一些阻碍,我们也会持续改进。客户端特定的设置(配置文件位置、重启行为、日志位置)请查阅该客户端自己的文档,而非此 README:

不同客户端最常见的问题在于 command 字段:客户端启动子进程时通常使用精简的 PATH,而不是你的 shell 使用的 PATH,因此即使在终端中可以正常运行,直接写 "command": "magicmarkets" 也可能无法解析。如果遇到这种情况,请改用绝对路径:

which magicmarkets   # or: make where
通过本地主机 HTTP 提供服务

传入 --http,即可使用可流式传输的 HTTP 传输方式,而不是 stdio——适用于客户端通过网络连接的情况,或希望多个客户端共享一个长期运行的服务,而不是为每个客户端启动一个子进程的情况:

magicmarkets mcp --http --addr 127.0.0.1:8383

--addr 默认为 127.0.0.1:8383,仅限环回地址,因此无论如何,机器外部都无法访问。--http 本身不提供 TLS——如果要将其暴露在环回地址之外,请将其置于反向代理之后。

该服务不会使用 MAGICMARKETS_API_KEY。 每个请求都必须通过 X-Api-Key 或 Authorization: Bearer 发送调用者的凭据。API 密钥会原样转发到 Magic Markets REST API 和 /v2/stream。Bearer 令牌则不会原样转发——它会先通过 POST {MAGICMARKETS_OAUTH_ISSUER}/oauth2/firebase-token 解析为它们实际需要的凭据;完整流程及已知限制请参阅身份验证。stdio 仍会从环境中读取 MAGICMARKETS_API_KEY 或 MAGICMARKETS_ACCESS_TOKEN。

将客户端指向 URL,而不是命令:

{
  "mcpServers": {
    "magicmarkets": {
      "url": "http://127.0.0.1:8383/mcp",
      "headers": { "X-Api-Key": "your-key" }
    }
  }
}

同一 URL 也接受 OAuth 访问令牌:

{
  "mcpServers": {
    "magicmarkets": {
      "url": "http://127.0.0.1:8383/mcp",
      "headers": { "Authorization": "Bearer your-token" }
    }
  }
}

支持 OAuth 的远程 MCP 主机可以省略 headers 映射。未经身份验证的 /mcp 响应会包含 WWW-Authenticate,指向受保护资源的元数据;该元数据会将此 MCP 主机指定为授权服务器(因此 Claude 的动态客户端注册会向此处的 /register 发送 POST,而不是发送到 https://magicmarkets.com/register)。此进程会针对 https://magicmarkets.com/api/auth 自行执行 PKCE 登录——它绝不会将下游客户端的 redirect_uri 转发到上游,因为真正的 Magic Markets AS 只会将此主机自己的回调地址({public-url}/mcp/callback)加入允许列表,而不会允许 Claude 或 Cursor 的回调地址。请设置:

  • 在托管部署中设置 --public-url https://magicmarkets-mcp.dev-eu.kubershmuber.com
  • 将 MAGICMARKETS_OAUTH_CLIENT_ID 设置为 MAGICMARKETS_OAUTH_ISSUER 上的客户端 ID,且该客户端的 redirect_uris 允许列表中包含此主机的 /mcp/callback
  • 在多副本部署的每个副本上设置 MAGICMARKETS_OAUTH_PROXY_SECRET——代理不会保留服务端会话状态(登录状态和一次性代码会被封装为自包含令牌),因此未共享此密钥的副本无法解码彼此正在进行的登录
启用交易

默认情况下,交易处于关闭状态。 新注册为只读,因此 Agent 请求下注时,不会找到 place_order 工具。要启用交易,请设置 MAGICMARKETS_ALLOW_TRADING=1,替换现有注册,并重启客户端:

claude mcp remove magicmarkets
claude mcp add magicmarkets -e MAGICMARKETS_API_KEY=your-key -e MAGICMARKETS_ALLOW_TRADING=1 -- magicmarkets mcp

重启很重要:客户端会在启动时读取一次子进程的工具列表,因此,即使重新注册后,已在运行的会话仍会保留只读列表。

直接编辑客户端的 MCP 配置文件时,请在 env 中设置 MAGICMARKETS_ALLOW_TRADING:

{
  "mcpServers": {
    "magicmarkets": {
      "command": "magicmarkets",
      "args": ["mcp"],
      "env": {
        "MAGICMARKETS_API_KEY": "your-key",
        "MAGICMARKETS_ALLOW_TRADING": "1"
      }
    }
  }
}

无需使用客户端,即可确认当前模式:

$ MAGICMARKETS_ALLOW_TRADING=1 magicmarkets mcp --print-tools
mode: trading ENABLED (via MAGICMARKETS_ALLOW_TRADING) — this process can place real bets

TOOL
----
close_all_orders
close_order
create_betslip
place_order
...

不设置环境变量运行即可查看只读列表(11 个工具,而非 19 个)。magicmarkets mcp 每次启动时也会将模式记录到 stderr,这些内容会显示在客户端的 MCP 日志中(这些日志只能写入 stderr——stdout 是 JSON-RPC 流)。

各模式提供的工具
始终可用需要设置 MAGICMARKETS_ALLOW_TRADING
get_balance、get_exchange_rates、get_positioncreate_betslip
list_events、list_event_offersplace_order
list_orders、get_orderclose_order、close_all_orders
list_betslips、get_betslipcreate_heartbeat、refresh_heartbeat、cancel_heartbeat、list_heartbeats
validate_bet_type、snap_price

只有在 Agent 需要下注真钱时才启用交易。涉及花钱的工具带有 MCP 破坏性提示,因此客户端会在调用前提示确认。

请注意,此限制仅适用于 magicmarkets mcp。CLI 自带的 magicmarkets order place 始终可用——它有自己的确认提示。

错误

错误会携带稳定且可供机器读取的 code,以便据此分支处理:

HTTPCode含义
400validation_error请求正文或查询参数未通过验证;会列出各字段的原因
400order_closed订单存在,但已关闭或结算
401auth_error密钥缺失、格式错误或遭到拒绝
403forbidden密钥有效,但不允许执行此操作
404not_found资源未知,或此密钥无法查看
409order_already_created重用了 request_uuid;会报告现有订单 ID
409limit_reached达到单个客户的上限
429throttled请求受到速率限制;会自动重试,并遵循 Retry-After
500server_error内部错误;报告时请提供支持令牌

受速率限制的请求会自动重试(默认两次),因为 429 表示请求被直接拒绝,因此没有创建任何内容。其他状态码都不会触发重试。

幂等性
uuid=$(uuidgen)
magicmarkets order place --betslip bs-123 --price 2.10 --stake 50 --request-uuid "$uuid"
magicmarkets order tracked "$uuid"     # recover after a timeout, safely

如果检测到重用了 UUID,magicmarkets 会获取并显示原有订单,而不是报错。

速率限制

每个账户采用滑动窗口:整体突发速率为 100 req/s,持续速率为 1200 req/min;下注单创建和下单另有专用额度,分别为 10 req/s 和 5 req/s。

故障排除

no API key configured —— 设置 MAGICMARKETS_API_KEY。magicmarkets status 会显示已读取哪些 .env 文件。

auth_error(401) —— 完全没有发送密钥。请检查变量名称。

每次调用都出现 session_not_found(404) —— 已发送密钥,但无法识别。请在 Settings → API 下重新生成密钥。

stream handshake failed —— WebSocket 在 HTTP 握手阶段拒绝了错误的密钥。请先运行 magicmarkets status;REST 会提供更清楚的错误信息。

magicmarkets markets 未返回任何内容 —— 快照仅包含当前有实时赔率的赛事,不包含完整的赛程列表。请尝试不使用 --sport,或调高 --timeout。

下注单没有赔率 —— 报价会异步送达。请使用 --wait 5s。如果仍然没有赔率,则没有来源在为该选项报价。

updated_at_to must be at least 60 seconds in the past —— magicmarkets order updates 的时间窗口必须在至少 60 秒前结束,且跨度不得超过 70 分钟。

Agent 表示无法下注 / 需要 MAGICMARKETS_ALLOW_TRADING —— magicmarkets mcp 子进程以只读模式运行,因此未注册下注工具。请参阅启用交易:设置 MAGICMARKETS_ALLOW_TRADING=1 重新注册,并重启客户端。使用 magicmarkets mcp --print-tools 检查当前模式。


开发

目录结构

cmd/magicmarkets/main.go          entry point — signal handling, version
internal/config/           .env + environment resolution
internal/magicmarkets/            API client — no CLI or MCP dependencies
  client.go                transport, envelope, 429 retry
  errors.go                typed APIError per error code
  types.go                 wire types (Stake is a [ccy, amount] tuple)
  ticks.go                 tick schedule and price snapping
  betslips.go orders.go account.go heartbeats.go
  stream.go                WebSocket client
internal/cli/              cobra command tree, table/JSON rendering
internal/mcpserver/        MCP tools over stdio or localhost HTTP, same client
internal/spec/             embedded openapi.json + reference commands
internal/magicmarketsapi/         generated models + the contract test guarding drift
tools/prepspec/            adapts the spec for oapi-codegen
docs/api-reference.md      full API reference (vendored)

internal/magicmarkets 不依赖 CLI 或 MCP 层,因此可作为普通的 Go 客户端库使用。

日常命令

make test           # go test ./...
make lint           # go vet ./...
make fmt            # gofmt -w
make build          # ./build/magicmarkets
make install        # install `magicmarkets` into your Go bin directory
make where          # print where make install puts the binary
make generate       # regenerate internal/magicmarketsapi from the vendored spec
make update-spec    # refresh the vendored spec + docs, then regenerate

测试不需要 API 密钥或网络。请保持这一点。

主包位于 cmd/magicmarkets/,而非模块根目录,因此生成的二进制文件名为 magicmarkets。如果构建根目录,二进制文件会以模块路径命名——即 magicmarkets-cli——这与文档或 magicmarkets --help 中告知你运行的名称不符。新增构建目标时,请确保它们指向 $(PKG)。

make build 和 make install 会使用 git describe 为 main.version 写入版本信息,因此 magicmarkets --version 会报告可追溯的版本。可通过 make build VERSION=v1.2.3 覆盖。

代码生成

internal/magicmarketsapi 中的模型由 vendored OpenAPI 规范通过 oapi-codegen 生成。

无需设置。 oapi-codegen 由 go.mod 中的 tool 指令固定版本,因此在全新克隆的仓库中运行 make generate 即可。生成的文件已纳入版本控制,所以执行 git clone && go build 无需代码生成工具。

make generate                 # regenerate from internal/spec/openapi.json
make update-spec              # pull the latest spec from the API, then regenerate

规范的权威来源是 magicmarkets.com/magic-api/docs——make update-spec 会获取 /magic-api/v2/openapi.json 以及 Markdown 参考文档。之后运行 git diff,即可查看 API 的具体变更。

这些生成的类型是契约参考,不是 CLI 实际使用的类型

internal/magicmarkets 中的客户端保留了手写类型,因为生成的代码无法表达此 API 所需的三件事:

  • Stake 元组。 ["USDT", 115.38] 是 OpenAPI 3.1 元组;oapi-codegen 完全无法生成这种类型。
  • 投注状态联合类型。 投注的状态要么是一个纯字符串,要么是一个对象。magicmarkets.BetStatus 可以反序列化这两种形式;生成的联合类型则会把这种分支处理推给每个调用方。
  • 非指针访问。 规范几乎没有将任何字段标记为 required,因此每个生成的字段都是指针。对于 API 始终会发送的字段,在 CLI 中到处添加 nil 检查只会增加干扰。
如何确保两者保持一致

internal/magicmarketsapi/contract_test.go 会双向比较每个手写类型与其生成类型的 JSON 字段名,任何差异都会导致测试失败。上游字段一旦新增、删除或重命名,运行 make generate 后,go test 就会报错,而不是等到运行时才发现。

它第一次运行就发挥了作用:发现 Order 中缺少 bet_bar_values,导致 magicmarkets order get --json 悄无声息地丢弃了该字段。

如果测试失败,说明规范与客户端已不一致。修复客户端,或者在对应项的 specOnly / handOnly 映射中记录例外,并说明原因。不要为了让测试通过而删除该项。

tools/prepspec 处理的两处特殊情况

它会在代码生成前调整规范,不会修改 vendored 文件:

  • number → float64。 oapi-codegen 会将未指定格式的 OpenAPI number 映射为 float32(约 7 位有效数字),精度不足以表示价格和投注额。prepspec 会添加 format: double。这也包括可空的 ["number", "null"] 形式,恰好涵盖已实现价格字段。有一项测试会断言生成的金额字段绝不会是 float32。
  • 将 StakeTuple 展平成无类型数组,因为 oapi-codegen 遇到 3.1 元组会直接失败。magicmarkets.Stake 才是真正的强类型对应项。

不要手动编辑 internal/magicmarketsapi/types.gen.go。

约定和不变量

本代码库依赖以下事项。若要破坏其中任何一项,都应审慎决定。

绝不要为了测试更改而下真实订单。 magicmarkets order place、magicmarkets order close* 以及 MCP place_order / close_* 工具都会花费真实资金。只读命令(status、balance、xrates、markets、offers、orders、position)和离线的 magicmarkets api 命令可以安全使用。对于写入操作,请使用本地桩服务器。

推断数据结构前先检查规范。 magicmarkets api show orders POST 比猜测更可靠。有几个端点不遵循常见的 {status, data} 模式,每个例外都曾是一个只有阅读规范才能发现的 bug:

  • GET /v2/heartbeats/ 会将数据封装在 heartbeats 键下;其他所有列表端点都会返回扁平数组。
  • POST /v2/orders/{id}/close/ 始终返回 data: null。要获取订单的最终状态,请重新读取订单。
  • POST /v2/betslips/{id}/refresh/ 没有文档说明的响应体,因此 RefreshBetslip 会重新读取 betslip,而不是解码响应。

保持分层结构。 internal/magicmarkets 不得导入 internal/cli 或 internal/mcpserver。

金额使用 float64,价格必须经过 SnapPrice 处理。 绝不要在价格或投注额处理路径中引入 float32。

会花费资金的新 MCP 工具必须受 AllowTrading 控制,并带有破坏性提示。该门控有测试覆盖;不要削弱它。

涉及资金的代码必须有测试。 ticks.go 和 MCP 交易门控都有测试,用于断言安全属性——价格取整绝不会放宽投注者的限价,且未设置 MAGICMARKETS_ALLOW_TRADING 时无法使用交易工具。请扩展这些测试,而不是绕过它们。

每条命令都支持 --json,否则会以表格形式输出。数据写入 stdout;警告和提示写入 stderr,以确保管道输出干净。

根据错误代码分支,而不是根据字符串。 使用 magicmarkets.HasCode(err, magicmarkets.CodeOrderClosed)。

身份验证

此仓库面向 public v2 API:https://magicmarkets.com/v2。调用方提供两种凭据之一:X-Api-Key,或使用来自 https://magicmarkets.com/api/auth 的令牌作为 Authorization: Bearer;但实际上只有 API 密钥会发送到网络上。v2 API 或 /v2/stream 均不接受原样的 Bearer 令牌;必须先通过两步将其解析为这些端点所需的 magic-metadata-jwt/session 组合:

  1. POST {issuer}/oauth2/firebase-token(Authorization: Bearer <access token>)会签发一个携带玩家真实权益的 Firebase 自定义令牌。
  2. Firebase 自定义令牌本身不是有效的 ID 令牌——Magic Markets 面向 MCP 服务实现者的 OAuth 集成指南明确指出,自定义令牌必须先兑换为 Firebase ID 令牌才能使用。此流程会自行完成兑换,直接调用 Google 的 Identity Toolkit REST API——POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=<MAGICMARKETS_FIREBASE_WEB_API_KEY>——并将返回的 idToken 用作 magic-metadata-jwt。

此流程不会使用 GET {issuer}/me:该接口受 Firebase ID 令牌验证保护,而自签名 OAuth 访问令牌无法通过验证。internal/magicmarkets.MeResolver(internal/magicmarkets/meauth.go)会完成这两步并缓存结果;internal/mcpserver 和 CLI/stdio 路径都使用同一个客户端,因此会自动获得该结果。magicmarkets mcp --http 会提供 OAuth 受保护资源元数据,让 MCP 主机(Claude、Cursor 等)能够获取 Bearer 令牌。

**为何需要 MAGICMARKETS_FIREBASE_WEB_API_KEY:**上面的第 2 步是调用 Firebase 的 Identity Toolkit API,而非 Magic Markets 的任何端点;Firebase 要求提供一个 Web API 密钥——该密钥限定于 Magic Markets Firebase 项目——以识别要兑换自定义令牌的项目。它不是为每个调用方单独签发的密钥;任何 Firebase Web 客户端都会在客户端内嵌入同一个项目级密钥。由于该密钥与项目和环境有关(STG 和 prod 属于不同的 Firebase 项目),没有可安全使用的默认值。因此,与 MAGICMARKETS_SESSION_GROUP_ID 一样,凡是预期接受 OAuth Bearer 调用的地方都必须显式设置该密钥,否则所有通过 Bearer 身份验证的调用都会失败。

sequenceDiagram
    participant Caller
    participant Resolver as MeResolver (cache)
    participant Issuer as Magic Markets AS
    participant Firebase as Firebase Identity Toolkit
    participant API as v2 API / stream

    Note over Caller,API: An X-Api-Key credential skips all of this, forwarded unchanged.

    Caller->>Resolver: Authorization Bearer access token

    alt cache hit, MeCacheTTL is 1 minute
        Resolver->>Resolver: reuse cached magic-metadata-jwt and session
    else cache miss
        Resolver->>Issuer: POST /oauth2/firebase-token<br/>Authorization Bearer access token
        Issuer-->>Resolver: firebase_token, a Firebase custom token<br/>not yet valid as magic-metadata-jwt
        Resolver->>Firebase: POST accounts:signInWithCustomToken<br/>key is MAGICMARKETS_FIREBASE_WEB_API_KEY, token is firebase_token
        Firebase-->>Resolver: idToken
        Resolver->>Resolver: cache magic-metadata-jwt as idToken<br/>session as m, group id, uuid joined by dashes<br/>uuid read from the access token's own sub claim
    end

    Resolver-->>Caller: magic-metadata-jwt, session
    Caller->>API: REST headers magic-metadata-jwt and session<br/>stream query params jwt and token
已知限制
  • 此流程会自行将 Firebase 自定义令牌兑换为 ID 令牌,而不是把这件事交给 Magic Markets 处理。 POST /oauth2/firebase-token 同样可以在服务端调用 signInWithCustomToken,并返回可直接使用的 ID 令牌 — 这样每个 MCP 服务实现者(不只是这个项目)就都不必再配置 MAGICMARKETS_FIREBASE_WEB_API_KEY、直接依赖 Google 的 Identity Toolkit 端点,也不必了解自定义令牌与 ID 令牌之间的区别。这是针对授权服务器契约缺失之处的客户端变通方案,并非预期的最终状态 — 如果 /oauth2/firebase-token 开始返回 ID 令牌(或 v2 API 开始接受自定义令牌),届时再重新审视。
  • 缓存仅在进程内,不会共享。 MeResolver 的缓存是按副本设置的 LRU 缓存(使用 hashicorp/golang-lru 的可过期版本),而不是 internal/mcpserver/oauth.go 用于 OAuth 代理状态的密封式、与副本无关的状态。在托管的多副本部署中,如果请求被路由到与先前请求不同的 Pod,只会多进行一次兑换(现在是两次调用:/oauth2/firebase-token 和 signInWithCustomToken),因为缓存未命中 — 不会像未共享的 MAGICMARKETS_OAUTH_PROXY_SECRET 那样导致失败。这是有意采用的变通方案,并非最终状态:共享缓存(或 Magic Markets 提供的、有效期更长且有文档说明的凭据)可以彻底消除各副本的冷启动开销。
  • TTL 和缓存大小是固定常量,不是环境变量(magicmarkets.MeCacheTTL = 1 分钟;缓存大小为 4096 个不同的访问令牌)— 参见 internal/magicmarkets/meauth.go。在 /me 契约本身仍在完善期间,这样做可以让变通方案保持简单;等到有必要调优时再重新审视。
  • MAGICMARKETS_SESSION_GROUP_ID 和 MAGICMARKETS_FIREBASE_WEB_API_KEY 没有安全的默认值,且因环境而异 — 在预期会有 OAuth Bearer 调用方的所有环境中,都必须显式设置这两个变量,否则所有通过 Bearer 身份验证的调用都会失败。
  • 不会主动刷新令牌。 每次缓存未命中时都会重试解析,但不会在访问令牌过期前刷新它 — 过期令牌会导致兑换失败(进而导致工具调用失败),与其他无效凭据的情况相同。

进行更改

请参阅 CONTRIBUTING.md,了解分支 → 代码 → 检查 → PR 工作流,包括更改涉及供应商提供的 OpenAPI 规范时该如何处理。


许可证

MIT — 参见 LICENSE。

由 Magic Markets 维护。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。