
MagicMarkets
com.magicmarketsv1.0.0更新於 Oct 7, 2026
Sports prediction markets for AI agents — live prices, quotes, orders, and positions.
概覽
讓助理讀取運動預測市場的即時價格、報價、訂單與持倉,並可選擇下真實資金的注單。
- 功能
- MagicMarkets 把 Magic Markets 運動預測市場 API 包裝成 MCP 工具。唯讀工具涵蓋餘額、匯率、持倉、賽事與報價清單、訂單、注單、投注類型驗證與價格對齊。啟用交易後,還會增加建立注單、下單、關閉單筆或全部訂單,以及管理心跳的工具;心跳是一種失聯開關,未及時更新時會自動關閉所有未結訂單。
- 適用情境
- 適合讓助理監控運動預測市場價格、檢視未結訂單與持倉,或執行自動化投注策略。若只是取得一般網路或運動資料,則不需要它,它只針對這個市場與帳戶。
- 執行需求
- 此伺服器以遠端端點方式存取,或透過 magicmarkets 命令列工具在本機執行,後者是從程式碼倉庫複製後安裝的 Go 執行檔。需要帳戶設定中建立的 API 金鑰:stdio 方式用環境變數 MAGICMARKETS_API_KEY,HTTP 方式用標頭 X-Api-Key;也可改用 OAuth Bearer 權杖。需要能連線至 Magic Markets API 與 WebSocket 行情流的網路。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 MagicMarkets,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Web executable,透過 Streamable HTTP。 遠端服務在工作區中設定後即可從網頁執行環境執行。
其他 MCP 客戶端
把它新增到你客戶端的 mcpServers 設定中。
{
"mcpServers": {
"mcp": {
"type": "http",
"url": "https://magicmarkets.com/mcp"
}
}
}README
magicmarkets-cli
A command-line interface for the Magic Markets v2 API — stream live sports prices, quote selections, place and manage orders, and inspect your position. The same API is also available as MCP tools, over stdio (a client launches magicmarkets mcp as a subprocess) or the streamable HTTP transport on localhost.
Single static binary, authenticated with one API key. No request signing, no private keys.
- Setup — install, authenticate, first commands
- Using it — the bet flow, command reference, MCP, prices, errors
- Development — layout, code generation, conventions, contributing
Setup
1. Install
From a clone:
make where prints exactly where that landed. If magicmarkets is not found afterwards, that directory is not on your PATH:
Prefer not to install? make build produces ./build/magicmarkets and leaves your PATH alone.
go install directly
Note the /cmd/magicmarkets path — installing the module root would build a binary called magicmarkets-cli:
The Go module path is magicmarkets-cli, not a GitHub URL, so go install github.com/…/magicmarkets-cli@latest will not work. Cloning and make install is the supported path.
2. Add your API key
Create a key at magicmarkets.com under Settings → API. It is shown once at creation, so store it immediately.
Put it in ~/.magicmarkets/.env so it works from any directory:
An env var or a project-local .env works too — cp env.example .env and fill it in.
3. Verify
Always check this before opening a stream — the WebSocket rejects a bad key at the handshake without a useful error.
That's the whole setup. Everything below is optional.
Try it
Add --json to any command to pipe it into jq.
Configuration
Resolved in this order, first match winning:
Global flags: --json, --verbose/-v, --api-key, --api-url, --ws-url.
--api-url re-derives the stream endpoint from it (matching MAGICMARKETS_WS_URL's
own derivation), unless MAGICMARKETS_WS_URL or --ws-url pins it explicitly —
so --api-url https://staging... doesn't leave stream reading production
prices while every other command reaches staging.
Using it
The two-step bet flow
Placing a bet always takes two steps:
- A betslip registers interest in one selection and receives a live quote. It costs nothing and commits nothing.
- An order commits a stake against that quote.
Betslips are short-lived and carry no price when created — the quote arrives asynchronously, over the WebSocket as a pmm message or by polling. Hence --wait:
Never construct a bet_type by hand. Copy it verbatim from magicmarkets offers or the stream — it encodes the market, handicap, outcome and direction in one string.
Command reference
Account
Discovery
The v2 REST API has no event-listing endpoint — discovery happens over the WebSocket. These commands connect, read the snapshot, and disconnect, so they take a few seconds.
Trading
Lay bets and parlays:
Risk
A heartbeat is a dead-man's switch: if it is not refreshed before it expires, every open order is closed automatically. Run one alongside an automated strategy so a crash cannot leave orders live.
On Ctrl-C the heartbeat is cancelled cleanly, leaving orders open. If the process dies, the switch fires.
MCP
Reference
The magicmarkets api commands need no API key and no network — the OpenAPI 3.1 spec is compiled into the binary.
Safe-operation checklist
Worth internalising before running anything that spends money:
- Pass
--request-uuidon every order. It makes placement idempotent: a retry after a timeout cannot create a second order, and the order stays retrievable for six hours. Without it, a timeout leaves you unsure whether a bet was placed. - Run a heartbeat when automating. Without one, a crashed strategy leaves orders live in the market.
- Verify the key over REST before opening a stream. The WebSocket fails the handshake with no useful error.
- Take
bet_typefrom the feed, never by hand. Asian handicap lines are 4× the real line, so a hand-built string is easy to get silently wrong. - Check the snapped price.
magicmarkets order placeshows it in the confirmation; that is the price the order actually runs with, not what you typed.
Prices and the tick schedule
Every price lies on a fixed tick schedule whose step widens as the price grows:
An off-tick order price is rounded so it never tightens your limit: down for back (for) orders, up for lay (against) orders. magicmarkets order place snaps the price itself and shows the result in the confirmation.
Prices quoted from the feed are already on the schedule and are never re-rounded.
Bet type grammar
bet_type is a comma-separated string beginning with the direction: for to back, against to lay. Handicaps always refer to the home team.
Asian handicap lines are integers equal to 4× the real line — -4 is -1.0, 2 is +0.5, 7 is +1.75. This keeps 0.25-step lines integer-only on the wire.
Validate any candidate string:
The full grammar — tennis periods, time-period tokens, every market — is in docs/api-reference.md.
JSON output
Every command takes --json:
Stakes are two-element tuples, not objects — --json mirrors the API wire
format exactly, so index them rather than reaching for a field name:
The same applies to every money field: want_stake, stake, profit_loss, total, and the min/max inside a price level.
MCP
magicmarkets mcp serves the API as MCP tools, so an LLM agent can read prices and manage orders. By default it speaks stdio: an MCP client (Claude Code, Cursor, and the like) launches it as a subprocess and they exchange JSON-RPC on stdin/stdout. Pass --http to instead serve the MCP streamable HTTP transport on localhost — see Serving over localhost HTTP below.
stdio
Register the command with a client:
Or in a client's MCP config — mcpServers is the client's name for a stdio subprocess, not a network service:
MCP support is developer-mode for now — expect some friction wiring a stdio server into a given client, and expect that to keep improving. For client-specific setup (where the config file lives, restart behavior, log locations), see that client's own docs rather than this README:
- Claude Code: Connect to tools via MCP
- Model Context Protocol: Connect to local (stdio) servers — covers Claude Desktop and other MCP clients generically
Across clients, the most common snag is the command field: a client often spawns the subprocess with a minimal PATH, not your shell's, so a bare "command": "magicmarkets" can fail to resolve even though the same command works from a terminal. If that happens, swap in the absolute path instead:
Serving over localhost HTTP
Pass --http to serve the streamable HTTP transport instead of stdio — useful when a client connects over the network, or you want one long-running server shared by several clients instead of a subprocess per client:
--addr defaults to 127.0.0.1:8383 — loopback-only, so nothing outside the machine can reach it regardless. --http has no TLS of its own — put it behind a reverse proxy if you expose it beyond loopback.
The server does not use MAGICMARKETS_API_KEY. Each request must send the caller's credential as X-Api-Key or Authorization: Bearer. An API key is forwarded to the Magic Markets REST API and /v2/stream unchanged. A Bearer token is not — it's resolved first, via POST {MAGICMARKETS_OAUTH_ISSUER}/oauth2/firebase-token, to the credential those actually require; see Authentication for the full flow and its known limitations. Stdio still takes MAGICMARKETS_API_KEY or MAGICMARKETS_ACCESS_TOKEN from the environment.
Point a client at the URL instead of a command:
The same URL accepts an OAuth access token:
Remote MCP hosts that speak OAuth can skip the headers map. Unauthenticated /mcp replies include WWW-Authenticate pointing at protected-resource metadata that names this MCP host as the Authorization Server (so Claude's Dynamic Client Registration POSTs /register here, not https://magicmarkets.com/register). This process runs its own PKCE login against https://magicmarkets.com/api/auth — it never forwards a downstream client's redirect_uri upstream, since the real Magic Markets AS only allowlists this host's own callback ({public-url}/mcp/callback), never Claude's or Cursor's. Set:
--public-url https://magicmarkets-mcp.dev-eu.kubershmuber.comon the hosted deployMAGICMARKETS_OAUTH_CLIENT_IDto a client onMAGICMARKETS_OAUTH_ISSUERwhose redirect_uris allowlist includes that host's/mcp/callbackMAGICMARKETS_OAUTH_PROXY_SECRETon every replica of a multi-replica deployment — the proxy keeps no server-side session state (login state and one-time codes are sealed, self-contained tokens instead), so replicas that don't share this secret can't decode each other's in-flight logins
Enabling trading
Trading is off by default. A fresh registration is read-only, so an agent asking to place a bet will find no place_order tool at all. Enable it with MAGICMARKETS_ALLOW_TRADING=1, replace the existing registration, and restart your client:
Restarting matters: a client reads the subprocess's tool list once at startup, so an already-running session keeps the read-only list even after you re-register.
Editing a client's MCP config file directly, set MAGICMARKETS_ALLOW_TRADING in env:
Confirm which mode you are in without involving a client:
Run it without the env var to see the read-only list (11 tools vs 19). magicmarkets mcp also logs the mode to stderr on every start, which appears in your client's MCP logs (stderr is the only place those lines can go — stdout is the JSON-RPC stream).
What each mode exposes
Enable trading only if the agent should be able to bet real money. The money-spending tools carry MCP destructive hints so clients prompt before calling them.
Note this gate applies to magicmarkets mcp only. The CLI's own magicmarkets order place is always available — it has its own confirmation prompt instead.
Errors
Errors carry a stable machine-readable code to branch on:
Throttled requests are retried automatically (twice by default) because a 429 means the request was rejected outright, so nothing was created. No other status is retried.
Idempotency
If a reused UUID is detected, magicmarkets fetches and shows the original order instead of failing.
Rate limits
Per account, sliding window: 100 req/s burst and 1200 req/min sustained overall, with dedicated budgets of 10 req/s for betslip creation and 5 req/s for order placement.
Troubleshooting
no API key configured — set MAGICMARKETS_API_KEY. magicmarkets status shows which .env files were read.
auth_error (401) — no key was sent at all. Check the variable name.
session_not_found (404) on every call — the key was sent but is not recognised. Regenerate it under Settings → API.
stream handshake failed — the WebSocket rejects a bad key at the HTTP handshake. Run magicmarkets status first; REST gives a clearer error.
magicmarkets markets returns nothing — the snapshot only contains events that currently have live prices, not the full fixture list. Try without --sport, or raise --timeout.
Betslip has no prices — quotes arrive asynchronously. Use --wait 5s. If it still has none, no source is quoting that selection.
updated_at_to must be at least 60 seconds in the past — magicmarkets order updates windows must end ≥60s ago and span ≤70 minutes.
An agent says it cannot bet / needs MAGICMARKETS_ALLOW_TRADING — the magicmarkets mcp subprocess is running read-only, so the betting tools are not registered. See Enabling trading: re-register with MAGICMARKETS_ALLOW_TRADING=1 and restart the client. Check the current mode with magicmarkets mcp --print-tools.
Development
Layout
internal/magicmarkets has no dependency on the CLI or MCP layers, so it is usable as a plain Go client library.
Everyday commands
Tests need no API key and no network. Keep it that way.
The main package lives in cmd/magicmarkets/, not the module root, so the binary is
named magicmarkets. Building the root would name it after the module path —
magicmarkets-cli — which is not what the docs or magicmarkets --help tell you to run. Keep
new build targets pointed at $(PKG).
make build and make install stamp main.version from git describe, so
magicmarkets --version reports something traceable. Override with
make build VERSION=v1.2.3.
Code generation
Models in internal/magicmarketsapi are generated from the vendored OpenAPI spec with oapi-codegen.
Setup: none. oapi-codegen is pinned by the tool directive in go.mod, so make generate works on a fresh clone. The generated file is checked in, so git clone && go build never requires codegen.
The canonical spec comes from magicmarkets.com/magic-api/docs — make update-spec fetches /magic-api/v2/openapi.json plus the Markdown reference. Run git diff afterwards to see exactly what the API changed.
These generated types are a contract reference, not what the CLI uses
The client in internal/magicmarkets keeps hand-written types, because generated code cannot express three things this API needs:
- Stake tuples.
["USDT", 115.38]is an OpenAPI 3.1 tuple; oapi-codegen cannot generate one at all. - The bet-status union. A bet's status is either a bare string or an object.
magicmarkets.BetStatusunmarshals both; a generated union type pushes that branch onto every caller. - Non-pointer access. The spec marks almost nothing
required, so every generated field is a pointer. Threading nil checks through the CLI for fields the API always sends would be noise.
What keeps the two honest
internal/magicmarketsapi/contract_test.go compares the JSON field names of every hand-written type against its generated counterpart, in both directions, and fails on any difference. An upstream field added, removed or renamed breaks go test after make generate instead of being discovered at runtime.
It earned its keep on the first run: it caught bet_bar_values missing from Order, which was silently dropping a field from magicmarkets order get --json.
If it fails, the spec and the client have diverged. Fix the client, or record the exception in that pair's specOnly / handOnly map with a reason. Do not delete the pair to make it pass.
Two wrinkles handled by tools/prepspec
It adapts the spec before codegen without touching the vendored file:
number→float64. oapi-codegen maps a formatless OpenAPInumbertofloat32(~7 significant digits), not enough for prices and stakes. prepspec addsformat: double. This includes the nullable["number", "null"]form, which covers precisely the achieved-price fields. A test asserts no generated money field is everfloat32.StakeTupleflattened to an untyped array, since oapi-codegen fails outright on a 3.1 tuple.magicmarkets.Stakeis the real typed equivalent.
Do not edit internal/magicmarketsapi/types.gen.go by hand.
Conventions and invariants
Things this codebase relies on. Breaking one should be deliberate.
Never place a real order to test a change. magicmarkets order place, magicmarkets order close*, and the MCP place_order / close_* tools spend real money. Read-only commands (status, balance, xrates, markets, offers, orders, position) and the offline magicmarkets api commands are safe to exercise. For write paths, use a local stub server.
Check the spec before inferring a shape. magicmarkets api show orders POST beats guessing. Several endpoints break the common {status, data} pattern, and each break was a bug caught only by reading the spec:
GET /v2/heartbeats/wraps data under aheartbeatskey; every other list endpoint returns a flat array.POST /v2/orders/{id}/close/always returnsdata: null. Re-read the order for its final state.POST /v2/betslips/{id}/refresh/has no documented response body, soRefreshBetslipre-reads the betslip instead of decoding the reply.
Keep the layering. internal/magicmarkets must not import internal/cli or internal/mcpserver.
Money is float64, and prices go through SnapPrice. Never introduce float32 on a price or stake path.
New MCP tools that spend money go behind AllowTrading and carry a destructive hint. The gate is tested; do not weaken it.
Money-touching code needs a test. ticks.go and the MCP trading gate both have tests asserting safety properties — a snap never tightens the bettor's limit, and trading tools are unreachable without MAGICMARKETS_ALLOW_TRADING. Extend those rather than working around them.
Every command supports --json and renders a table otherwise. Data goes to stdout; warnings and prompts go to stderr, so piping stays clean.
Branch on error codes, not strings. Use magicmarkets.HasCode(err, magicmarkets.CodeOrderClosed).
Authentication
This repo targets the public v2 API: https://magicmarkets.com/v2. A caller presents one of two credentials — X-Api-Key, or Authorization: Bearer with a token from https://magicmarkets.com/api/auth — but only the API key is what actually goes out on the wire. A Bearer token is not accepted by the v2 API or /v2/stream as-is; it must first be resolved, in two hops, to the magic-metadata-jwt/session pair those endpoints require:
POST {issuer}/oauth2/firebase-token(Authorization: Bearer <access token>) mints a Firebase custom token carrying the player's real entitlements.- A Firebase custom token is not itself a valid ID token — Magic Markets' own OAuth integration guide for MCP server implementers is explicit that a custom token must be redeemed for a Firebase ID token before it's usable. This process does that redemption itself, calling Google's Identity Toolkit REST API directly —
POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=<MAGICMARKETS_FIREBASE_WEB_API_KEY>— and uses the returnedidTokenasmagic-metadata-jwt.
GET {issuer}/me is not used for any of this: it's guarded by Firebase ID-token verification, which the self-signed OAuth access token never satisfies. internal/magicmarkets.MeResolver (internal/magicmarkets/meauth.go) does both hops and caches the result; both internal/mcpserver and the CLI/stdio path go through the same client, so both get it automatically. magicmarkets mcp --http advertises OAuth protected-resource metadata so MCP hosts (Claude, Cursor, ...) can obtain a Bearer token in the first place.
Why MAGICMARKETS_FIREBASE_WEB_API_KEY exists: hop 2 above is a call to Firebase's Identity Toolkit API, not to any Magic Markets endpoint, and Firebase requires a Web API key — scoped to the Magic Markets Firebase project — to identify which project's custom token is being redeemed. It is not a secret minted per-caller; it's the same project-level key any Firebase web client already embeds client-side. It has no safe default because it's project- and environment-specific (STG and prod are different Firebase projects), so — like MAGICMARKETS_SESSION_GROUP_ID — it must be set explicitly wherever an OAuth Bearer caller is expected, or every Bearer-authenticated call fails.
Known limitations
- This process redeems a Firebase custom token for an ID token itself, instead of that being Magic Markets' problem.
POST /oauth2/firebase-tokencould just as easily callsignInWithCustomTokenserver-side and hand back a ready-to-use ID token — sparing every MCP server implementer (not just this one) from needingMAGICMARKETS_FIREBASE_WEB_API_KEY, a direct dependency on Google's Identity Toolkit endpoint, and knowledge of the custom-token/ID-token distinction at all. This is a client-side workaround for a gap in the Authorization Server's contract, not the intended end state — revisit once/if/oauth2/firebase-tokenreturns an ID token (or the v2 API accepts a custom token directly). - The cache is in-process, not shared.
MeResolver's cache is a per-replica LRU (viahashicorp/golang-lru's expirable variant), not the sealed, replica-independent stateinternal/mcpserver/oauth.gouses for OAuth proxy state. On a hosted, multi-replica deployment, a request routed to a different pod than a prior one just pays for one extra exchange (now two calls:/oauth2/firebase-tokenandsignInWithCustomToken) on a cache miss — it does not fail, unlike an un-sharedMAGICMARKETS_OAUTH_PROXY_SECRETwould. This is a deliberate workaround, not the end state: a shared cache (or a documented, longer-lived credential from Magic Markets) would remove the per-replica cold-start cost entirely. - The TTL and cache size are fixed consts, not environment variables (
magicmarkets.MeCacheTTL= 1 minute; a size of 4096 distinct access tokens) — seeinternal/magicmarkets/meauth.go. This keeps the workaround simple while the/mecontract itself is still firming up; revisit once it's worth tuning. MAGICMARKETS_SESSION_GROUP_IDandMAGICMARKETS_FIREBASE_WEB_API_KEYhave no safe default and differ per environment — both must be set explicitly wherever an OAuth Bearer caller is expected, or every Bearer-authenticated call fails.- No proactive token refresh. Resolution is retried on every cache miss, but nothing refreshes an access token before it expires — an expired token surfaces as a failed exchange (and thus a failed tool call), the same as any other invalid credential.
Making a change
See CONTRIBUTING.md for the branch → code → check → PR workflow, including what to do if your change touches the vendored OpenAPI spec.
License
MIT — see LICENSE.
Maintained by Magic Markets.
來源:README.md,提交 c4a1827
工具
0版本歷史
1- v1.0.0最新Oct 7, 2026

