MagicMarkets

Mercados de previsão esportiva para agentes de IA — preços ao vivo, cotações, ordens e posições.

Documentação

magicmarkets-cli

Uma interface de linha de comando para a API Magic Markets v2 — transmita preços esportivos ao vivo, selecione cotações, coloque e gerencie ordens e inspecione sua posição. A mesma API também está disponível como ferramentas MCP, via stdio (um cliente inicia o magicmarkets mcp como subprocesso) ou pelo transporte HTTP transmissível em localhost.

Binário estático único, autenticado com uma chave de API. Sem assinatura de requisições, sem chaves privadas.

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
  • Configuração — instalação, autenticação, primeiros comandos
  • Como usar — fluxo de apostas, referência de comandos, MCP, preços, erros
  • Desenvolvimento — estrutura, geração de código, convenções, contribuição

Configuração

1. Instalação

A partir de um clone:

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

make where mostra exatamente onde foi instalado. Se magicmarkets não for encontrado depois, esse diretório não está no seu PATH:

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

Prefere não instalar? make build produz ./build/magicmarkets e não altera seu PATH.

go install diretamente

Observe o caminho /cmd/magicmarkets — instalar a raiz do módulo criaria um binário chamado magicmarkets-cli:

go install ./cmd/magicmarkets

O caminho do módulo Go é magicmarkets-cli, não uma URL do GitHub, então go install github.com/…/magicmarkets-cli@latest não funcionará. Clonar e executar make install é o caminho suportado.

2. Adicione sua chave de API

Crie uma chave em magicmarkets.com em Configurações → API. Ela é exibida apenas uma vez na criação, então armazene-a imediatamente.

Coloque-a em ~/.magicmarkets/.env para que funcione a partir de qualquer diretório:

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

Uma variável de ambiente ou um .env local ao projeto também funciona — cp env.example .env e preencha.

3. Verificação

$ 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

Sempre verifique isso antes de abrir um stream — o WebSocket rejeita uma chave inválida no handshake sem uma mensagem de erro útil.

Isso é toda a configuração. Tudo abaixo é opcional.

Experimente

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)

Adicione --json a qualquer comando para canalizá-lo para jq.

Configuração

Resolvida nesta ordem, vencendo a primeira correspondência:

PrioridadeFonte
1Variáveis de ambiente reais
2./.env
3~/.magicmarkets/.env
4~/.env
5Padrões integrados
VariávelPadrãoFinalidade
MAGICMARKETS_API_KEY—Chave de API (X-Api-Key). Obrigatória, a menos que MAGICMARKETS_ACCESS_TOKEN esteja definida
MAGICMARKETS_ACCESS_TOKEN—Token Bearer OAuth para CLI/stdio. O MCP HTTP ainda recebe o token por requisição
MAGICMARKETS_OAUTH_ISSUERhttps://magicmarkets.com/api/authAS upstream para o qual mcp --http faz proxy de /authorize e /token, e contra o qual POST /oauth2/firebase-token é resolvido para uma credencial Bearer — veja Autenticação
MAGICMARKETS_SESSION_GROUP_ID—ID específico do ambiente da Magic Markets, usado para construir o valor session ao qual uma credencial Bearer OAuth resolve. Sem padrão seguro — obrigatório onde uma credencial Bearer for esperada
MAGICMARKETS_FIREBASE_WEB_API_KEY—Chave da API Web do Firebase para o projeto Firebase da Magic Markets, usada para trocar o token customizado do Firebase de /oauth2/firebase-token por um token de ID Firebase real — veja Autenticação. Sem padrão seguro — obrigatório onde uma credencial Bearer for esperada
MAGICMARKETS_OAUTH_CLIENT_ID—Cliente pré-registrado que permite o próprio /mcp/callback deste host
MAGICMARKETS_OAUTH_PROXY_SECRET—Selo do estado do proxy OAuth; toda réplica deve compartilhá-lo
MAGICMARKETS_MCP_PUBLIC_URL—Base pública de mcp --http (este host é o Servidor de Autorização MCP)
MAGICMARKETS_API_URLhttps://magicmarkets.com/v2Base REST, incluindo /v2
MAGICMARKETS_BASIC_AUTH—user:pass em Base64 enviado como cabeçalho adicional Authorization: Basic em toda chamada REST /v2/* — o token para uma barreira de infraestrutura que alguns ambientes não produtivos colocam diante da API v2. Produção não tem essa barreira, então isso fica indefinido lá. Nunca enviado para /oauth2/firebase-token, signInWithCustomToken do Firebase ou o stream WebSocket
MAGICMARKETS_WS_URLderivado de MAGICMARKETS_API_URLEndpoint do stream
MAGICMARKETS_LANGenIdioma do nome do evento: en, ko, zh-hans
MAGICMARKETS_TIMEOUT30sTempo limite por requisição
MAGICMARKETS_ALLOW_TRADINGindefinido (desligado)Permite que magicmarkets mcp coloque apostas. Sem efeito na CLI.

Flags globais: --json, --verbose/-v, --api-key, --api-url, --ws-url.

--api-url rederiva o endpoint do stream a partir dele (correspondendo à própria derivação de MAGICMARKETS_WS_URL), a menos que MAGICMARKETS_WS_URL ou --ws-url o fixe explicitamente — então --api-url https://staging... não deixa stream lendo preços de produção enquanto todos os outros comandos alcançam o ambiente de staging.


Como usar

O fluxo de aposta em duas etapas

Colocar uma aposta sempre exige duas etapas:

  1. Um betslip registra interesse em uma seleção e recebe uma cotação ao vivo. Não custa nada e não compromete nada.
  2. Uma ordem compromete um valor contra essa cotação.

Betslips são de curta duração e não têm preço quando criados — a cotação chega de forma assíncrona, pelo WebSocket como uma mensagem pmm ou por polling. Daí o --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     -   150.00@2.10  80.00@2.08     230.00
for,ah,h,-4       ah      -   200.00@1.95  120.00@1.94    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

Nunca construa um bet_type manualmente. Copie-o literalmente de magicmarkets offers ou do stream — ele codifica mercado, handicap, resultado e direção em uma única string.

Referência de comandos

Conta

ComandoFinalidade
magicmarkets statusMostrar configuração e verificar a chave
magicmarkets balanceSaldo, valor aberto, crédito inteligente, disponível
magicmarkets xratesTaxas de câmbio para USDT
magicmarkets positionP&L agregado, com --grid para a matriz de pagamento

Descoberta

ComandoFinalidade
magicmarkets marketsEventos que atualmente têm preços
magicmarkets offers <sport> <event-id>Tipos de aposta com preço em um evento
magicmarkets streamAcompanhar o feed ao vivo de preços e da conta

A API REST v2 não tem endpoint de listagem de eventos — a descoberta acontece pelo WebSocket. Esses comandos conectam, leem o snapshot e desconectam, então levam alguns segundos.

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

Negociação

ComandoFinalidade
magicmarkets betslip create [sport] [event] [bet-type]Cotar uma seleção
magicmarkets betslip get <id>Mostrar um betslip e seus preços
magicmarkets betslip listIDs de betslips abertos (--expand para detalhes completos)
magicmarkets betslip refresh <id>Estender expiração
magicmarkets order placeColocar uma ordem contra um betslip
magicmarkets orders / magicmarkets order listListar ordens
magicmarkets order get <id>Mostrar uma ordem e suas apostas
magicmarkets order tracked <uuid>Buscar uma ordem pela chave de idempotência
magicmarkets order updatesOrdens alteradas em uma janela de tempo
magicmarkets order close <id>Cancelar uma ordem
magicmarkets order close-many <id>...Cancelar até 500 ordens
magicmarkets order close-allCancelar todas as ordens abertas

Apostas lay e combinadas:

# 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

Risco

ComandoFinalidade
magicmarkets heartbeat runCriar um heartbeat e mantê-lo ativo em primeiro plano
magicmarkets heartbeat create/list/get/refresh/cancelGerenciar heartbeats diretamente

Um heartbeat é um interruptor de segurança: se não for renovado antes de expirar, toda ordem aberta é fechada automaticamente. Execute um junto com uma estratégia automatizada para que uma falha não deixe ordens ativas.

$ 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

No Ctrl-C, o heartbeat é cancelado de forma limpa, deixando as ordens abertas. Se o processo morrer, o interruptor dispara.

MCP

ComandoFinalidade
magicmarkets mcpFerramentas MCP via stdio por padrão, ou --http para o transporte HTTP transmissível em localhost. Veja MCP

Referência

ComandoFinalidade
magicmarkets bet-type <sport> <bet-type>Validar um tipo de aposta, mostrar sua grade de pagamento
magicmarkets ticks <price>Ajustar um preço ao cronograma de ticks
magicmarkets api endpointsListar todos os endpoints
magicmarkets api show <path> [method]Detalhe do endpoint: parâmetros, corpo, respostas
magicmarkets api schema [name]Esquemas de componentes
magicmarkets api curl <method> <path>Gerar um comando curl executável
magicmarkets api search <term>Buscar endpoints e esquemas
magicmarkets api specImprimir a especificação OpenAPI embutida

Os comandos magicmarkets api não precisam de chave de API nem de rede — a especificação OpenAPI 3.1 é compilada no binário.

Checklist de operação segura

Vale internalizar antes de executar qualquer coisa que gaste dinheiro:

  • Passe --request-uuid em toda ordem. Isso torna a colocação idempotente: uma nova tentativa após timeout não pode criar uma segunda ordem, e a ordem permanece recuperável por seis horas. Sem isso, um timeout deixa você em dúvida se a aposta foi colocada.
  • Execute um heartbeat ao automatizar. Sem um, uma estratégia que falha deixa ordens ativas no mercado.
  • Verifique a chave via REST antes de abrir um stream. O WebSocket falha no handshake sem erro útil.
  • Pegue bet_type do feed, nunca manualmente. Linhas de handicap asiático são 4× a linha real, então uma string construída à mão é fácil de errar silenciosamente.
  • Confira o preço ajustado. magicmarkets order place o mostra na confirmação; esse é o preço com o qual a ordem realmente executa, não o que você digitou.

Preços e o cronograma de ticks

Todo preço está em um cronograma de ticks fixo cujo passo aumenta conforme o preço cresce:

Preço decimalTick
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

Um preço de ordem fora do tick é arredondado para nunca apertar seu limite: para baixo em ordens back (for), para cima em ordens lay (against). magicmarkets order place ajusta o preço por conta própria e mostra o resultado na confirmação.

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

Preços cotados pelo feed já estão no cronograma e nunca são rearredondados.

Gramática de tipos de aposta

bet_type é uma string separada por vírgulas que começa com a direção: for para back, against para lay. Handicaps sempre se referem ao time da casa.

ExemploSignificado
for,h / for,d / for,aVitória da casa / empate / vitória fora
for,dnb,hVitória da casa, anulada se empate
for,dc,h,dChance dupla: casa ou empate
for,over,2.5 / for,under,2.5Mais/menos 2,5 gols
for,ah,h,-4Handicap asiático, casa -1,0
for,ahover,7Total asiático acima de 1,75
for,cs,2,1Placar exato 2–1
for,score,both,yesAmbas as equipes marcam
for,win,<team_id>Participante vence um mercado outright
for,top,3,<team_id>Participante termina no top 3

Linhas de handicap asiático são inteiros iguais a 4× a linha real — -4 é -1,0, 2 é +0,5, 7 é +1,75. Isso mantém linhas com passo de 0,25 como inteiros apenas no fio.

Valide qualquer string candidata:

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

A gramática completa — períodos de tênis, tokens de período de tempo, todos os mercados — está em docs/api-reference.md.

Saída JSON

Todo comando aceita --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

Valores são tuplas de dois elementos, não objetos — --json espelha o formato do fio da API exatamente, então indexe-os em vez de buscar um nome de campo:

$ 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

O mesmo se aplica a todo campo monetário: want_stake, stake, profit_loss, total e o min/max dentro de um nível de preço.

MCP

magicmarkets mcp serve a API como ferramentas MCP, para que um agente LLM possa ler preços e gerenciar ordens. Por padrão, fala stdio: um cliente MCP (Claude Code, Cursor e similares) o inicia como subprocesso e eles trocam JSON-RPC em stdin/stdout. Passe --http para, em vez disso, servir o transporte HTTP transmissível do MCP em localhost — veja Servindo via HTTP em localhost abaixo.

stdio

Registre o comando em um cliente:

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

Ou no arquivo de configuração MCP de um cliente — mcpServers é o nome do cliente para um subprocesso stdio, não um serviço de rede:

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

O suporte a MCP está em modo de desenvolvedor por enquanto — espere alguma fricção ao conectar um servidor stdio a um cliente específico, e espere que isso continue melhorando. Para configuração específica de cliente (onde o arquivo de configuração fica, comportamento de reinicialização, locais de log), consulte a documentação do próprio cliente em vez deste README:

Entre clientes, o problema mais comum é o campo command: um cliente frequentemente inicia o subprocesso com um PATH mínimo, não o do seu shell, então um "command": "magicmarkets" simples pode falhar ao resolver mesmo que o mesmo comando funcione em um terminal. Se isso acontecer, use o caminho absoluto:

which magicmarkets   # or: make where

Servindo via HTTP localhost

Passe --http para servir o transporte HTTP streamable em vez de stdio — útil quando um cliente se conecta pela rede, ou você quer um servidor de longa duração compartilhado por vários clientes em vez de um subprocesso por cliente:

magicmarkets mcp --http --addr 127.0.0.1:8383

--addr padrão é 127.0.0.1:8383 — somente loopback, então nada fora da máquina pode alcançá-lo de qualquer forma. --http não tem TLS próprio — coloque-o atrás de um proxy reverso se você o expor além do loopback.

O servidor não usa MAGICMARKETS_API_KEY. Cada requisição deve enviar a credencial do chamador como X-Api-Key ou Authorization: Bearer. Uma chave de API é encaminhada para a API REST da Magic Markets e /v2/stream inalterada. Um token Bearer não é — ele é resolvido primeiro, via POST {MAGICMARKETS_OAUTH_ISSUER}/oauth2/firebase-token, para a credencial que eles realmente exigem; veja Authentication para o fluxo completo e suas limitações conhecidas. Stdio ainda aceita MAGICMARKETS_API_KEY ou MAGICMARKETS_ACCESS_TOKEN do ambiente.

Aponte um cliente para a URL em vez de um comando:

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

A mesma URL aceita um token de acesso OAuth:

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

Hosts MCP remotos que falam OAuth podem pular o mapa de cabeçalhos. Respostas /mcp não autenticadas incluem WWW-Authenticate apontando para metadados de recurso protegido que nomeiam este host MCP como o Servidor de Autorização (então o Dynamic Client Registration do Claude faz POST /register aqui, não https://magicmarkets.com/register). Este processo executa seu próprio login PKCE contra https://magicmarkets.com/api/auth — ele nunca encaminha o redirect_uri de um cliente downstream para cima, já que o AS real da Magic Markets só permite o callback deste próprio host ({public-url}/mcp/callback), nunca o do Claude ou do Cursor. Defina:

  • --public-url https://magicmarkets-mcp.dev-eu.kubershmuber.com no deploy hospedado
  • MAGICMARKETS_OAUTH_CLIENT_ID para um cliente em MAGICMARKETS_OAUTH_ISSUER cuja allowlist de redirect_uris inclui o /mcp/callback desse host
  • MAGICMARKETS_OAUTH_PROXY_SECRET em cada réplica de um deploy multi-réplica — o proxy não mantém estado de sessão no servidor (estado de login e códigos de uso único são tokens selados e autocontidos), então réplicas que não compartilham este segredo não conseguem decodificar os logins em andamento umas das outras

Habilitando negociação

Negociação está desativada por padrão. Um registro novo é somente leitura, então um agente pedindo para fazer uma aposta não encontrará nenhuma ferramenta place_order. Habilite com MAGICMARKETS_ALLOW_TRADING=1, substitua o registro existente e reinicie seu cliente:

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

Reiniciar importa: um cliente lê a lista de ferramentas do subprocesso uma vez na inicialização, então uma sessão já em execução mantém a lista somente leitura mesmo depois de você re-registrar.

Editando o arquivo de configuração MCP de um cliente diretamente, defina MAGICMARKETS_ALLOW_TRADING em env:

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

Confirme em qual modo você está sem envolver um cliente:

$ 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
...

Execute sem a variável de ambiente para ver a lista somente leitura (11 ferramentas vs 19). magicmarkets mcp também registra o modo no stderr a cada início, o que aparece nos logs MCP do seu cliente (stderr é o único lugar onde essas linhas podem ir — stdout é o fluxo JSON-RPC).

O que cada modo expõe

Sempre disponívelRequer 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

Habilite a negociação apenas se o agente deve ser capaz de apostar dinheiro real. As ferramentas que gastam dinheiro carregam dicas destrutivas MCP para que os clientes peçam confirmação antes de chamá-las.

Observe que esta porta se aplica apenas a magicmarkets mcp. O próprio magicmarkets order place da CLI está sempre disponível — ele tem seu próprio prompt de confirmação.

Erros

Erros carregam um code estável legível por máquina para ramificar:

HTTPCódigoSignificado
400validation_errorCorpo ou consulta falhou na validação; razões por campo são impressas
400order_closedPedido existe mas já está fechado ou liquidado
401auth_errorChave ausente, malformada ou rejeitada
403forbiddenChave válida mas ação não permitida
404not_foundRecurso desconhecido ou invisível para esta chave
409order_already_createdUm request_uuid foi reutilizado; o ID do pedido existente é relatado
409limit_reachedUm limite por cliente foi atingido
429throttledLimitado por taxa; repetido automaticamente, respeitando Retry-After
500server_errorErro interno; cite o token de suporte ao relatar

Requisições limitadas são repetidas automaticamente (duas vezes por padrão) porque um 429 significa que a requisição foi rejeitada completamente, então nada foi criado. Nenhum outro status é repetido.

Idempotência

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

Se um UUID reutilizado for detectado, magicmarkets busca e mostra o pedido original em vez de falhar.

Limites de taxa

Por conta, janela deslizante: 100 req/s de rajada e 1200 req/min sustentados no geral, com orçamentos dedicados de 10 req/s para criação de betslip e 5 req/s para colocação de pedidos.

Solução de problemas

no API key configured — defina MAGICMARKETS_API_KEY. magicmarkets status mostra quais arquivos .env foram lidos.

auth_error (401) — nenhuma chave foi enviada. Verifique o nome da variável.

session_not_found (404) em toda chamada — a chave foi enviada mas não é reconhecida. Regere-a em Configurações → API.

stream handshake failed — o WebSocket rejeita uma chave ruim no handshake HTTP. Execute magicmarkets status primeiro; REST dá um erro mais claro.

magicmarkets markets retorna nada — o snapshot só contém eventos que atualmente têm preços ao vivo, não a lista completa de fixtures. Tente sem --sport, ou aumente --timeout.

Betslip sem preços — cotações chegam assincronamente. Use --wait 5s. Se ainda não tiver nenhuma, nenhuma fonte está cotando essa seleção.

updated_at_to must be at least 60 seconds in the past — janelas magicmarkets order updates devem terminar ≥60s atrás e durar ≤70 minutos.

Um agente diz que não pode apostar / precisa de MAGICMARKETS_ALLOW_TRADING — o subprocesso magicmarkets mcp está rodando somente leitura, então as ferramentas de aposta não estão registradas. Veja Enabling trading: re-registre com MAGICMARKETS_ALLOW_TRADING=1 e reinicie o cliente. Verifique o modo atual com magicmarkets mcp --print-tools.


Desenvolvimento

Layout

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 não tem dependência das camadas CLI ou MCP, então é utilizável como uma biblioteca Go cliente simples.

Comandos do dia a dia

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

Testes não precisam de chave de API nem rede. Mantenha assim.

O pacote principal fica em cmd/magicmarkets/, não na raiz do módulo, então o binário é nomeado magicmarkets. Construir a raiz o nomearia pelo caminho do módulo — magicmarkets-cli — o que não é o que os docs ou magicmarkets --help dizem para executar. Mantenha novos alvos de build apontados para $(PKG).

make build e make install carimbam main.version de git describe, então magicmarkets --version relata algo rastreável. Substitua com make build VERSION=v1.2.3.

Geração de código

Modelos em internal/magicmarketsapi são gerados a partir da especificação OpenAPI vendida com oapi-codegen.

Configuração: nenhuma. oapi-codegen é fixado pela diretiva tool em go.mod, então make generate funciona em um clone novo. O arquivo gerado é versionado, então git clone && go build nunca requer codegen.

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

A especificação canônica vem de magicmarkets.com/magic-api/docs — make update-spec busca /magic-api/v2/openapi.json mais a referência Markdown. Execute git diff depois para ver exatamente o que a API mudou.

Esses tipos gerados são uma referência de contrato, não o que a CLI usa

O cliente em internal/magicmarkets mantém tipos escritos à mão, porque código gerado não consegue expressar três coisas que esta API precisa:

  • Tuplas de stake. ["USDT", 115.38] é uma tupla OpenAPI 3.1; oapi-codegen não consegue gerar uma.
  • A união de status de aposta. O status de uma aposta é ou uma string simples ou um objeto. magicmarkets.BetStatus desserializa ambos; um tipo de união gerado empurra esse ramo para todo chamador.
  • Acesso não ponteiro. A especificação marca quase nada como required, então todo campo gerado é um ponteiro. Passar verificações de nil pela CLI para campos que a API sempre envia seria ruído.

O que mantém os dois honestos

internal/magicmarketsapi/contract_test.go compara os nomes de campos JSON de cada tipo escrito à mão contra sua contraparte gerada, em ambas as direções, e falha em qualquer diferença. Um campo upstream adicionado, removido ou renomeado quebra go test após make generate em vez de ser descoberto em tempo de execução.

Ele provou seu valor na primeira execução: pegou bet_bar_values faltando em Order, que estava silenciosamente descartando um campo de magicmarkets order get --json.

Se falhar, a especificação e o cliente divergiram. Corrija o cliente, ou registre a exceção no mapa specOnly / handOnly desse par com uma razão. Não delete o par para fazê-lo passar.

Duas rugas tratadas por tools/prepspec

Ele adapta a especificação antes do codegen sem tocar no arquivo vendido:

  • number → float64. oapi-codegen mapeia um number OpenAPI sem formato para float32 (~7 dígitos significativos), não suficiente para preços e stakes. prepspec adiciona format: double. Isso inclui a forma anulável ["number", "null"], que cobre precisamente os campos de preço alcançado. Um teste afirma que nenhum campo de dinheiro gerado é nunca float32.
  • StakeTuple achatado para um array sem tipo, já que oapi-codegen falha completamente em uma tupla 3.1. magicmarkets.Stake é o equivalente tipado real.

Não edite internal/magicmarketsapi/types.gen.go à mão.

Convenções e invariantes

Coisas nas quais este código depende. Quebrar uma deve ser deliberado.

Nunca faça um pedido real para testar uma mudança. magicmarkets order place, magicmarkets order close*, e as ferramentas MCP place_order / close_* gastam dinheiro real. Comandos somente leitura (status, balance, xrates, markets, offers, orders, position) e os comandos offline magicmarkets api são seguros de exercitar. Para caminhos de escrita, use um servidor stub local.

Verifique a especificação antes de inferir uma forma. magicmarkets api show orders POST vence adivinhação. Vários endpoints quebram o padrão comum {status, data}, e cada quebra foi um bug pego apenas lendo a especificação:

  • GET /v2/heartbeats/ envolve dados sob uma chave heartbeats; todo outro endpoint de lista retorna um array plano.
  • POST /v2/orders/{id}/close/ sempre retorna data: null. Re-leia o pedido para seu estado final.
  • POST /v2/betslips/{id}/refresh/ não tem corpo de resposta documentado, então RefreshBetslip re-lê o betslip em vez de decodificar a resposta.

Mantenha as camadas. internal/magicmarkets não deve importar internal/cli ou internal/mcpserver.

Dinheiro é float64, e preços passam por SnapPrice. Nunca introduza float32 em um caminho de preço ou stake.

Novas ferramentas MCP que gastam dinheiro vão atrás de AllowTrading e carregam uma dica destrutiva. A porta é testada; não a enfraqueça. Código que lida com dinheiro precisa de um teste. ticks.go e o gate de negociação do MCP têm testes que verificam propriedades de segurança — um snap nunca aperta o limite do apostador, e as ferramentas de negociação são inacessíveis sem MAGICMARKETS_ALLOW_TRADING. Estenda esses testes em vez de contorná-los.

Todo comando suporta --json e renderiza uma tabela caso contrário. Dados vão para stdout; avisos e prompts vão para stderr, para que o pipe permaneça limpo.

Faça branch por códigos de erro, não por strings. Use magicmarkets.HasCode(err, magicmarkets.CodeOrderClosed).

Autenticação

Este repositório tem como alvo a API pública v2: https://magicmarkets.com/v2. Um chamador apresenta uma de duas credenciais — X-Api-Key, ou Authorization: Bearer com um token de https://magicmarkets.com/api/auth — mas apenas a chave de API é o que realmente vai para a rede. Um token Bearer não é aceito pela API v2 ou /v2/stream como está; ele deve primeiro ser resolvido, em dois saltos, para o par magic-metadata-jwt/session que esses endpoints exigem:

  1. POST {issuer}/oauth2/firebase-token (Authorization: Bearer <access token>) emite um token customizado do Firebase carregando as permissões reais do jogador.
  2. Um token customizado do Firebase não é por si só um token de ID válido — o próprio guia de integração OAuth da Magic Markets para implementadores de servidores MCP é explícito que um token customizado deve ser trocado por um token de ID do Firebase antes de ser utilizável. Este processo faz essa troca por conta própria, chamando a REST API do Identity Toolkit do Google diretamente — POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=<MAGICMARKETS_FIREBASE_WEB_API_KEY> — e usa o idToken retornado como magic-metadata-jwt.

GET {issuer}/me não é usado para nada disso: é protegido por verificação de token de ID do Firebase, que o token de acesso OAuth autoassinado nunca satisfaz. internal/magicmarkets.MeResolver (internal/magicmarkets/meauth.go) faz ambos os saltos e armazena o resultado em cache; tanto internal/mcpserver quanto o caminho CLI/stdio passam pelo mesmo cliente, então ambos o obtêm automaticamente. magicmarkets mcp --http anuncia metadados de recursos protegidos por OAuth para que hosts MCP (Claude, Cursor, ...) possam obter um token Bearer em primeiro lugar.

Por que MAGICMARKETS_FIREBASE_WEB_API_KEY existe: o salto 2 acima é uma chamada à API do Identity Toolkit do Firebase, não a qualquer endpoint da Magic Markets, e o Firebase exige uma chave de API Web — com escopo no projeto Firebase da Magic Markets — para identificar qual token customizado do projeto está sendo trocado. Não é um segredo emitido por chamador; é a mesma chave de nível de projeto que qualquer cliente web do Firebase já incorpora no lado do cliente. Não tem um padrão seguro porque é específica do projeto e do ambiente (STG e produção são projetos Firebase diferentes), então — como MAGICMARKETS_SESSION_GROUP_ID — deve ser definida explicitamente onde quer que um chamador OAuth Bearer seja esperado, ou toda chamada autenticada por Bearer falha.

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

Limitações conhecidas

  • Este processo troca um token customizado do Firebase por um token de ID por conta própria, em vez de isso ser problema da Magic Markets. POST /oauth2/firebase-token poderia facilmente chamar signInWithCustomToken no lado do servidor e devolver um token de ID pronto para uso — poupando todo implementador de servidor MCP (não apenas este) de precisar de MAGICMARKETS_FIREBASE_WEB_API_KEY, uma dependência direta do endpoint do Identity Toolkit do Google e conhecimento da distinção token customizado/token de ID. Este é um workaround no lado do cliente para uma lacuna no contrato do Servidor de Autorização, não o estado final pretendido — reavalie quando/se /oauth2/firebase-token retornar um token de ID (ou a API v2 aceitar um token customizado diretamente).
  • O cache é em processo, não compartilhado. O cache de MeResolver é um LRU por réplica (via variante expirável de hashicorp/golang-lru), não o estado selado e independente de réplica que internal/mcpserver/oauth.go usa para estado de proxy OAuth. Em uma implantação hospedada com múltiplas réplicas, uma requisição roteada para um pod diferente de um anterior apenas paga por uma troca extra (agora duas chamadas: /oauth2/firebase-token e signInWithCustomToken) em uma falha de cache — não falha, ao contrário de um MAGICMARKETS_OAUTH_PROXY_SECRET não compartilhado. Este é um workaround deliberado, não o estado final: um cache compartilhado (ou uma credencial documentada e de vida mais longa da Magic Markets) removeria o custo de cold-start por réplica inteiramente.
  • O TTL e o tamanho do cache são constantes fixas, não variáveis de ambiente (magicmarkets.MeCacheTTL = 1 minuto; um tamanho de 4096 tokens de acesso distintos) — veja internal/magicmarkets/meauth.go. Isso mantém o workaround simples enquanto o contrato /me ainda está se firmando; reavalie quando valer a pena ajustar.
  • MAGICMARKETS_SESSION_GROUP_ID e MAGICMARKETS_FIREBASE_WEB_API_KEY não têm padrão seguro e diferem por ambiente — ambos devem ser definidos explicitamente onde quer que um chamador OAuth Bearer seja esperado, ou toda chamada autenticada por Bearer falha.
  • Sem renovação proativa de token. A resolução é tentada novamente a cada falha de cache, mas nada renova um token de acesso antes de expirar — um token expirado surge como uma troca falha (e, portanto, uma chamada de ferramenta falha), o mesmo que qualquer outra credencial inválida.

Fazendo uma alteração

Veja CONTRIBUTING.md para o fluxo de trabalho branch → código → check → PR, incluindo o que fazer se sua alteração tocar a especificação OpenAPI vendida.


Licença

MIT — veja LICENSE.

Mantido por Magic Markets.