MagicMarkets

Mercados de predicción deportiva para agentes de IA: precios en vivo, cotizaciones, órdenes y posiciones.

Documentación

magicmarkets-cli

Una interfaz de línea de comandos para la API v2 de Magic Markets — transmite precios deportivos en vivo, cotiza selecciones, coloca y gestiona órdenes, e inspecciona tu posición. La misma API también está disponible como herramientas MCP, a través de stdio (un cliente lanza magicmarkets mcp como subproceso) o el transporte HTTP transmisible en localhost.

Binario estático único, autenticado con una clave API. Sin firma de solicitudes, sin claves 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
  • Configuración — instalación, autenticación, primeros comandos
  • Uso — el flujo de apuestas, referencia de comandos, MCP, precios, errores
  • Desarrollo — estructura, generación de código, convenciones, contribuciones

Configuración

1. Instalar

Desde un clon:

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

make where imprime exactamente dónde aterrizó. Si magicmarkets no se encuentra después, ese directorio no está en tu PATH:

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

¿Prefieres no instalar? make build produce ./build/magicmarkets y deja tu PATH intacto.

go install directamente

Nota la ruta /cmd/magicmarkets — instalar la raíz del módulo construiría un binario llamado magicmarkets-cli:

go install ./cmd/magicmarkets

La ruta del módulo Go es magicmarkets-cli, no una URL de GitHub, por lo que go install github.com/…/magicmarkets-cli@latest no funcionará. Clonar y make install es la ruta compatible.

2. Añade tu clave API

Crea una clave en magicmarkets.com bajo Configuración → API. Se muestra una sola vez al crearla, así que guárdala inmediatamente.

Ponla en ~/.magicmarkets/.env para que funcione desde cualquier directorio:

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

Una variable de entorno o un .env local al proyecto también funciona — cp env.example .env y complétalo.

3. Verificar

$ 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

Comprueba siempre esto antes de abrir una transmisión — el WebSocket rechaza una clave incorrecta en el protocolo de enlace sin un error útil.

Eso es toda la configuración. Todo lo siguiente es opcional.

Pruébalo

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)

Añade --json a cualquier comando para canalizarlo a jq.

Configuración

Se resuelve en este orden, ganando la primera coincidencia:

PrioridadFuente
1Variables de entorno reales
2./.env
3~/.magicmarkets/.env
4~/.env
5Valores predeterminados integrados
VariablePredeterminadoPropósito
MAGICMARKETS_API_KEY—Clave API (X-Api-Key). Requerida a menos que MAGICMARKETS_ACCESS_TOKEN esté establecida
MAGICMARKETS_ACCESS_TOKEN—Token Bearer OAuth para CLI/stdio. El HTTP MCP aún toma el token por solicitud
MAGICMARKETS_OAUTH_ISSUERhttps://magicmarkets.com/api/authAS ascendente al que mcp --http hace proxy /authorize y /token, y contra el que se resuelve POST /oauth2/firebase-token para una credencial Bearer — ver Autenticación
MAGICMARKETS_SESSION_GROUP_ID—Identificador específico del entorno de Magic Markets, utilizado para construir el valor session al que se resuelve una credencial Bearer OAuth. Sin valor predeterminado seguro — requerido dondequiera que se espere una credencial Bearer
MAGICMARKETS_FIREBASE_WEB_API_KEY—Clave API web de Firebase para el proyecto Firebase de Magic Markets, utilizada para intercambiar el token personalizado de Firebase de /oauth2/firebase-token por un token de ID de Firebase real — ver Autenticación. Sin valor predeterminado seguro — requerido dondequiera que se espere una credencial Bearer
MAGICMARKETS_OAUTH_CLIENT_ID—Cliente pre-registrado que permite en la lista blanca el /mcp/callback propio de este host
MAGICMARKETS_OAUTH_PROXY_SECRET—Sella el estado del proxy OAuth; cada réplica debe compartirlo
MAGICMARKETS_MCP_PUBLIC_URL—Base pública de mcp --http (este host es el Servidor de Autorización MCP)
MAGICMARKETS_API_URLhttps://magicmarkets.com/v2Base REST, incluyendo /v2
MAGICMARKETS_BASIC_AUTH—user:pass Base64 enviado como un encabezado Authorization: Basic adicional en cada llamada REST /v2/* — el token para un muro a nivel de infraestructura que algunos entornos no productivos colocan frente a la API v2. Producción no tiene tal muro, por lo que no está establecido allí. Nunca se envía a /oauth2/firebase-token, al signInWithCustomToken de Firebase, ni a la transmisión WebSocket
MAGICMARKETS_WS_URLderivado de MAGICMARKETS_API_URLExtremo de transmisión
MAGICMARKETS_LANGenIdioma del nombre del evento: en, ko, zh-hans
MAGICMARKETS_TIMEOUT30sTiempo de espera por solicitud
MAGICMARKETS_ALLOW_TRADINGsin establecer (desactivado)Permite que magicmarkets mcp coloque apuestas. Sin efecto en la CLI.

Banderas globales: --json, --verbose/-v, --api-key, --api-url, --ws-url.

--api-url re-deriva el extremo de transmisión a partir de él (coincidiendo con la propia derivación de MAGICMARKETS_WS_URL), a menos que MAGICMARKETS_WS_URL o --ws-url lo fijen explícitamente — así que --api-url https://staging... no deja que stream lea precios de producción mientras que cada otro comando alcanza el entorno de pruebas.


Uso

El flujo de apuestas en dos pasos

Colocar una apuesta siempre requiere dos pasos:

  1. Un betslip registra interés en una selección y recibe una cotización en vivo. No cuesta nada y no compromete nada.
  2. Una orden compromete una apuesta contra esa cotización.

Los betslips son de corta duración y no llevan precio al crearse — la cotización llega de forma asíncrona, a través del WebSocket como un mensaje pmm o mediante sondeo. De ahí --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 construyas un bet_type a mano. Cópialo textualmente de magicmarkets offers o de la transmisión — codifica el mercado, hándicap, resultado y dirección en una sola cadena.

Referencia de comandos

Cuenta

ComandoPropósito
magicmarkets statusMostrar configuración y verificar la clave
magicmarkets balanceSaldo, apuesta abierta, crédito inteligente, disponible
magicmarkets xratesTipos de cambio a USDT
magicmarkets positionP&L agregado, con --grid para la matriz de pagos

Descubrimiento

ComandoPropósito
magicmarkets marketsEventos que actualmente tienen precios
magicmarkets offers <sport> <event-id>Tipos de apuesta con precio en un evento
magicmarkets streamSeguir la transmisión en vivo de precios y cuenta

La API REST v2 no tiene extremo de listado de eventos — el descubrimiento ocurre a través del WebSocket. Estos comandos se conectan, leen la instantánea y se desconectan, por lo que tardan unos 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

Negociación

ComandoPropósito
magicmarkets betslip create [sport] [event] [bet-type]Cotizar una selección
magicmarkets betslip get <id>Mostrar un betslip y sus precios
magicmarkets betslip listIDs de betslips abiertos (--expand para detalle completo)
magicmarkets betslip refresh <id>Extender la expiración
magicmarkets order placeColocar una orden contra un betslip
magicmarkets orders / magicmarkets order listListar órdenes
magicmarkets order get <id>Mostrar una orden y sus apuestas
magicmarkets order tracked <uuid>Buscar una orden por clave de idempotencia
magicmarkets order updatesÓrdenes cambiadas en una ventana de tiempo
magicmarkets order close <id>Cancelar una orden
magicmarkets order close-many <id>...Cancelar hasta 500 órdenes
magicmarkets order close-allCancelar cada orden abierta

Apuestas en contra y 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

Riesgo

ComandoPropósito
magicmarkets heartbeat runCrear un latido y mantenerlo vivo en primer plano
magicmarkets heartbeat create/list/get/refresh/cancelGestionar latidos directamente

Un latido es un interruptor de hombre muerto: si no se refresca antes de que expire, cada orden abierta se cierra automáticamente. Ejecuta uno junto a una estrategia automatizada para que un fallo no pueda dejar órdenes activas.

$ 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

Con Ctrl-C el latido se cancela limpiamente, dejando las órdenes abiertas. Si el proceso muere, el interruptor se activa.

MCP

ComandoPropósito
magicmarkets mcpHerramientas MCP a través de stdio por defecto, o --http para el transporte HTTP transmisible en localhost. Ver MCP

Referencia

ComandoPropósito
magicmarkets bet-type <sport> <bet-type>Validar un tipo de apuesta, mostrar su cuadrícula de pagos
magicmarkets ticks <price>Ajustar un precio al programa de ticks
magicmarkets api endpointsListar cada extremo
magicmarkets api show <path> [method]Detalle del extremo: parámetros, cuerpo, respuestas
magicmarkets api schema [name]Esquemas de componentes
magicmarkets api curl <method> <path>Generar un comando curl ejecutable
magicmarkets api search <term>Buscar extremos y esquemas
magicmarkets api specImprimir la especificación OpenAPI integrada

Los comandos magicmarkets api no necesitan clave API ni red — la especificación OpenAPI 3.1 está compilada en el binario.

Lista de verificación de operación segura

Vale la pena internalizar antes de ejecutar cualquier cosa que gaste dinero:

  • Pasa --request-uuid en cada orden. Hace que la colocación sea idempotente: un reintento después de un tiempo de espera no puede crear una segunda orden, y la orden permanece recuperable durante seis horas. Sin él, un tiempo de espera te deja sin saber si se colocó una apuesta.
  • Ejecuta un latido al automatizar. Sin uno, una estrategia bloqueada deja órdenes activas en el mercado.
  • Verifica la clave a través de REST antes de abrir una transmisión. El WebSocket falla el protocolo de enlace sin un error útil.
  • Toma bet_type de la transmisión, nunca a mano. Las líneas de hándicap asiático son 4× la línea real, por lo que una cadena construida a mano es fácil de equivocar silenciosamente.
  • Comprueba el precio ajustado. magicmarkets order place lo muestra en la confirmación; ese es el precio con el que realmente se ejecuta la orden, no el que escribiste.

Precios y el programa de ticks

Cada precio se encuentra en un programa de ticks fijo cuyo paso se amplía a medida que el precio crece:

Precio 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

Un precio de orden fuera de tick se redondea para que nunca ajuste tu límite: hacia abajo para órdenes de back (for), hacia arriba para órdenes de lay (against). magicmarkets order place ajusta el precio por sí mismo y muestra el resultado en la confirmación.

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

Los precios cotizados desde la transmisión ya están en el programa y nunca se vuelven a redondear.

Gramática de tipos de apuesta

bet_type es una cadena separada por comas que comienza con la dirección: for para back, against para lay. Los hándicaps siempre se refieren al equipo local.

EjemploSignificado
for,h / for,d / for,aVictoria local / empate / victoria visitante
for,dnb,hVictoria local, anulada si empate
for,dc,h,dDoble oportunidad: local o empate
for,over,2.5 / for,under,2.5Más/menos de 2.5 goles
for,ah,h,-4Hándicap asiático, local -1.0
for,ahover,7Total asiático más de 1.75
for,cs,2,1Resultado exacto 2–1
for,score,both,yesAmbos equipos marcan
for,win,<team_id>Participante para ganar un torneo
for,top,3,<team_id>Participante para terminar entre los 3 primeros

Las líneas de hándicap asiático son enteros iguales a 4× la línea real — -4 es -1.0, 2 es +0.5, 7 es +1.75. Esto mantiene las líneas de paso 0.25 como enteros solo en el cable.

Valida cualquier cadena candidata:

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

La gramática completa — períodos de tenis, tokens de período de tiempo, cada mercado — está en docs/api-reference.md.

Salida JSON

Cada comando acepta --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

Las apuestas son tuplas de dos elementos, no objetos — --json refleja el formato del cable de la API exactamente, así que indéxalas en lugar de buscar un nombre 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

Lo mismo aplica a cada campo de dinero: want_stake, stake, profit_loss, total, y el min/max dentro de un nivel de precio.

MCP

magicmarkets mcp sirve la API como herramientas MCP, para que un agente LLM pueda leer precios y gestionar órdenes. Por defecto habla stdio: un cliente MCP (Claude Code, Cursor y similares) lo lanza como subproceso e intercambian JSON-RPC en stdin/stdout. Pasa --http para servir en su lugar el transporte HTTP transmisible de MCP en localhost — ver Servir a través de HTTP en localhost abajo.

stdio

Registra el comando con un cliente:

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

O en la configuración MCP de un cliente — mcpServers es el nombre del cliente para un subproceso stdio, no un servicio de red:

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

El soporte de MCP está en modo desarrollador por ahora: espera cierta fricción al conectar un servidor stdio a un cliente determinado, y espera que eso siga mejorando. Para la configuración específica del cliente (dónde vive el archivo de configuración, comportamiento de reinicio, ubicaciones de registros), consulta la documentación de ese cliente en lugar de este README:

En todos los clientes, el problema más común es el campo command: un cliente a menudo inicia el subproceso con un PATH mínimo, no el de tu shell, por lo que un "command": "magicmarkets" simple puede fallar al resolverse incluso si el mismo comando funciona desde una terminal. Si eso ocurre, usa la ruta absoluta en su lugar:

which magicmarkets   # or: make where

Servir a través de HTTP en localhost

Pasa --http para servir el transporte HTTP transmisible en lugar de stdio — útil cuando un cliente se conecta a través de la red, o quieres un servidor de larga duración compartido por varios clientes en lugar de un subproceso por cliente:

magicmarkets mcp --http --addr 127.0.0.1:8383

--addr por defecto es 127.0.0.1:8383 — solo loopback, por lo que nada fuera de la máquina puede alcanzarlo de todos modos. --http no tiene TLS propio — colócalo detrás de un proxy inverso si lo expones más allá del loopback.

El servidor no usa MAGICMARKETS_API_KEY. Cada solicitud debe enviar la credencial del llamante como X-Api-Key o Authorization: Bearer. Una clave API se reenvía a la API REST de Magic Markets y /v2/stream sin cambios. Un token Bearer no — se resuelve primero, a través de POST {MAGICMARKETS_OAUTH_ISSUER}/oauth2/firebase-token, a la credencial que esos realmente requieren; consulta Authentication para el flujo completo y sus limitaciones conocidas. Stdio todavía toma MAGICMARKETS_API_KEY o MAGICMARKETS_ACCESS_TOKEN del entorno.

Apunta un cliente a la URL en lugar de un comando:

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

La misma URL acepta un token de acceso OAuth:

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

Los hosts MCP remotos que hablan OAuth pueden omitir el mapa de encabezados. Las respuestas /mcp no autenticadas incluyen WWW-Authenticate que apunta a metadatos de recursos protegidos que nombran este host MCP como el Servidor de Autorización (por lo que el Registro Dinámico de Clientes de Claude envía POST /register aquí, no https://magicmarkets.com/register). Este proceso ejecuta su propio inicio de sesión PKCE contra https://magicmarkets.com/api/auth — nunca reenvía el redirect_uri de un cliente descendente hacia arriba, ya que el AS real de Magic Markets solo permite en la lista blanca el callback propio de este host ({public-url}/mcp/callback), nunca el de Claude o Cursor. Establece:

  • --public-url https://magicmarkets-mcp.dev-eu.kubershmuber.com en el despliegue alojado
  • MAGICMARKETS_OAUTH_CLIENT_ID a un cliente en MAGICMARKETS_OAUTH_ISSUER cuya lista blanca de redirect_uris incluya el /mcp/callback de ese host
  • MAGICMARKETS_OAUTH_PROXY_SECRET en cada réplica de un despliegue multi-réplica — el proxy no mantiene estado de sesión en el servidor (el estado de inicio de sesión y los códigos de un solo uso son tokens sellados y autocontenidos), por lo que las réplicas que no comparten este secreto no pueden decodificar los inicios de sesión en curso de las demás

Habilitar trading

El trading está desactivado por defecto. Un registro nuevo es de solo lectura, por lo que un agente que pida colocar una apuesta no encontrará ninguna herramienta place_order. Habilítalo con MAGICMARKETS_ALLOW_TRADING=1, reemplaza el registro existente y reinicia tu cliente:

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

Reiniciar importa: un cliente lee la lista de herramientas del subproceso una vez al inicio, por lo que una sesión ya en ejecución mantiene la lista de solo lectura incluso después de que te vuelvas a registrar.

Al editar el archivo de configuración MCP de un cliente directamente, establece MAGICMARKETS_ALLOW_TRADING en env:

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

Confirma en qué modo estás sin involucrar a un 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
...

Ejecútalo sin la variable de entorno para ver la lista de solo lectura (11 herramientas vs 19). magicmarkets mcp también registra el modo en stderr en cada inicio, que aparece en los registros MCP de tu cliente (stderr es el único lugar donde esas líneas pueden ir — stdout es el flujo JSON-RPC).

Qué expone cada modo

Siempre disponibleRequiere 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

Habilita el trading solo si el agente debería poder apostar dinero real. Las herramientas que gastan dinero llevan indicaciones destructivas de MCP para que los clientes pidan confirmación antes de llamarlas.

Ten en cuenta que esta puerta se aplica solo a magicmarkets mcp. El propio magicmarkets order place del CLI está siempre disponible — tiene su propio mensaje de confirmación en su lugar.

Errores

Los errores llevan un code legible por máquina y estable para ramificar:

HTTPCódigoSignificado
400validation_errorEl cuerpo o la consulta fallaron la validación; se imprimen las razones por campo
400order_closedLa orden existe pero ya está cerrada o liquidada
401auth_errorClave faltante, malformada o rechazada
403forbiddenClave válida pero acción no permitida
404not_foundRecurso desconocido o invisible para esta clave
409order_already_createdSe reutilizó un request_uuid; se informa el ID de orden existente
409limit_reachedSe alcanzó un límite por cliente
429throttledLímite de velocidad; se reintenta automáticamente, respetando Retry-After
500server_errorError interno; cita el token de soporte al informar

Las solicitudes limitadas se reintentan automáticamente (dos veces por defecto) porque un 429 significa que la solicitud fue rechazada por completo, por lo que no se creó nada. Ningún otro estado se reintenta.

Idempotencia

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

Si se detecta un UUID reutilizado, magicmarkets obtiene y muestra la orden original en lugar de fallar.

Límites de velocidad

Por cuenta, ventana deslizante: 100 req/s en ráfaga y 1200 req/min sostenidos en general, con presupuestos dedicados de 10 req/s para la creación de boletos de apuesta y 5 req/s para la colocación de órdenes.

Solución de problemas

no API key configured — establece MAGICMARKETS_API_KEY. magicmarkets status muestra qué archivos .env se leyeron.

auth_error (401) — no se envió ninguna clave. Verifica el nombre de la variable.

session_not_found (404) en cada llamada — la clave se envió pero no se reconoce. Regenera en Configuración → API.

stream handshake failed — el WebSocket rechaza una clave incorrecta en el protocolo de enlace HTTP. Ejecuta magicmarkets status primero; REST da un error más claro.

magicmarkets markets no devuelve nada — la instantánea solo contiene eventos que actualmente tienen precios en vivo, no la lista completa de partidos. Prueba sin --sport, o aumenta --timeout.

El boleto de apuesta no tiene precios — las cotizaciones llegan de forma asíncrona. Usa --wait 5s. Si aún no tiene ninguna, ninguna fuente está cotizando esa selección.

updated_at_to must be at least 60 seconds in the past — las ventanas magicmarkets order updates deben terminar ≥60s atrás y abarcar ≤70 minutos.

Un agente dice que no puede apostar / necesita MAGICMARKETS_ALLOW_TRADING — el subproceso magicmarkets mcp se está ejecutando en modo de solo lectura, por lo que las herramientas de apuesta no están registradas. Consulta Enabling trading: vuelve a registrar con MAGICMARKETS_ALLOW_TRADING=1 y reinicia el cliente. Verifica el modo actual con magicmarkets mcp --print-tools.


Desarrollo

Estructura

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 no tiene dependencia de las capas CLI o MCP, por lo que es utilizable como una biblioteca cliente Go simple.

Comandos cotidianos

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

Las pruebas no necesitan clave API ni red. Mantenlo así.

El paquete principal vive en cmd/magicmarkets/, no en la raíz del módulo, por lo que el binario se llama magicmarkets. Compilar la raíz lo nombraría según la ruta del módulo — magicmarkets-cli — que no es lo que los documentos o magicmarkets --help te dicen que ejecutes. Mantén los nuevos objetivos de compilación apuntando a $(PKG).

make build y make install sellan main.version desde git describe, por lo que magicmarkets --version informa algo trazable. Anula con make build VERSION=v1.2.3.

Generación de código

Los modelos en internal/magicmarketsapi se generan a partir de la especificación OpenAPI incluida con oapi-codegen.

Configuración: ninguna. oapi-codegen está fijado por la directiva tool en go.mod, por lo que make generate funciona en un clon nuevo. El archivo generado está verificado, por lo que git clone && go build nunca requiere codegen.

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

La especificación canónica proviene de magicmarkets.com/magic-api/docs — make update-spec obtiene /magic-api/v2/openapi.json más la referencia Markdown. Ejecuta git diff después para ver exactamente qué cambió en la API.

Estos tipos generados son una referencia de contrato, no lo que usa el CLI

El cliente en internal/magicmarkets mantiene tipos escritos a mano, porque el código generado no puede expresar tres cosas que esta API necesita:

  • Tuplas de apuesta. ["USDT", 115.38] es una tupla OpenAPI 3.1; oapi-codegen no puede generar una en absoluto.
  • La unión de estado de apuesta. El estado de una apuesta es una cadena simple o un objeto. magicmarkets.BetStatus deserializa ambos; un tipo de unión generado empuja esa rama a cada llamante.
  • Acceso sin puntero. La especificación marca casi nada como required, por lo que cada campo generado es un puntero. Pasar verificaciones de nil a través del CLI para campos que la API siempre envía sería ruido.

Qué mantiene honestos a ambos

internal/magicmarketsapi/contract_test.go compara los nombres de campo JSON de cada tipo escrito a mano contra su contraparte generada, en ambas direcciones, y falla en cualquier diferencia. Un campo ascendente agregado, eliminado o renombrado rompe go test después de make generate en lugar de descubrirse en tiempo de ejecución.

Se ganó su lugar en la primera ejecución: detectó bet_bar_values faltante en Order, que estaba eliminando silenciosamente un campo de magicmarkets order get --json.

Si falla, la especificación y el cliente han divergido. Arregla el cliente, o registra la excepción en el mapa specOnly / handOnly de ese par con una razón. No elimines el par para que pase.

Dos arrugas manejadas por tools/prepspec

Adapta la especificación antes del codegen sin tocar el archivo incluido:

  • number → float64. oapi-codegen mapea un number OpenAPI sin formato a float32 (~7 dígitos significativos), no suficiente para precios y apuestas. prepspec agrega format: double. Esto incluye la forma ["number", "null"] anulable, que cubre precisamente los campos de precio alcanzado. Una prueba afirma que ningún campo de dinero generado es nunca float32.
  • StakeTuple aplanado a un array sin tipo, ya que oapi-codegen falla por completo en una tupla 3.1. magicmarkets.Stake es el equivalente tipado real.

No edites internal/magicmarketsapi/types.gen.go a mano.

Convenciones e invariantes

Cosas en las que este código base confía. Romper una debería ser deliberado.

Nunca coloques una orden real para probar un cambio. magicmarkets order place, magicmarkets order close* y las herramientas MCP place_order / close_* gastan dinero real. Los comandos de solo lectura (status, balance, xrates, markets, offers, orders, position) y los comandos magicmarkets api fuera de línea son seguros de ejercitar. Para rutas de escritura, usa un servidor stub local.

Verifica la especificación antes de inferir una forma. magicmarkets api show orders POST supera a adivinar. Varios endpoints rompen el patrón común {status, data}, y cada ruptura fue un error detectado solo al leer la especificación:

  • GET /v2/heartbeats/ envuelve datos bajo una clave heartbeats; cada otro endpoint de lista devuelve un array plano.
  • POST /v2/orders/{id}/close/ siempre devuelve data: null. Vuelve a leer la orden para su estado final.
  • POST /v2/betslips/{id}/refresh/ no tiene cuerpo de respuesta documentado, por lo que RefreshBetslip vuelve a leer el boleto de apuesta en lugar de decodificar la respuesta.

Mantén las capas. internal/magicmarkets no debe importar internal/cli o internal/mcpserver.

El dinero es float64, y los precios pasan por SnapPrice. Nunca introduzcas float32 en una ruta de precio o apuesta.

Las nuevas herramientas MCP que gastan dinero van detrás de AllowTrading y llevan una indicación destructiva. La puerta está probada; no la debilites. El código que maneja dinero necesita una prueba. ticks.go y la puerta de negociación de MCP tienen pruebas que verifican propiedades de seguridad: un snap nunca ajusta el límite del apostador, y las herramientas de negociación son inaccesibles sin MAGICMARKETS_ALLOW_TRADING. Extiende esas pruebas en lugar de trabajar alrededor de ellas.

Cada comando soporta --json y renderiza una tabla en caso contrario. Los datos van a stdout; las advertencias y avisos van a stderr, para que el pipe se mantenga limpio.

Bifurca según códigos de error, no cadenas. Usa magicmarkets.HasCode(err, magicmarkets.CodeOrderClosed).

Autenticación

Este repositorio apunta a la API pública v2: https://magicmarkets.com/v2. Un llamador presenta una de dos credenciales — X-Api-Key, o Authorization: Bearer con un token de https://magicmarkets.com/api/auth — pero solo la clave de API es lo que realmente sale por el cable. Un token Bearer no es aceptado por la API v2 ni por /v2/stream tal cual; primero debe resolverse, en dos saltos, al par magic-metadata-jwt/session que esos endpoints requieren:

  1. POST {issuer}/oauth2/firebase-token (Authorization: Bearer <access token>) acuña un token personalizado de Firebase que lleva los derechos reales del jugador.
  2. Un token personalizado de Firebase no es en sí mismo un token de ID válido — la guía de integración OAuth de Magic Markets para implementadores de servidores MCP es explícita en que un token personalizado debe canjearse por un token de ID de Firebase antes de ser utilizable. Este proceso hace ese canje por sí mismo, llamando directamente a la API REST de Identity Toolkit de Google — POST https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=<MAGICMARKETS_FIREBASE_WEB_API_KEY> — y usa el idToken devuelto como magic-metadata-jwt.

GET {issuer}/me no se usa para nada de esto: está protegido por verificación de token de ID de Firebase, que el token de acceso OAuth autofirmado nunca satisface. internal/magicmarkets.MeResolver (internal/magicmarkets/meauth.go) hace ambos saltos y almacena en caché el resultado; tanto internal/mcpserver como la ruta CLI/stdio pasan por el mismo cliente, así que ambos lo obtienen automáticamente. magicmarkets mcp --http anuncia metadatos de recursos protegidos OAuth para que los hosts MCP (Claude, Cursor, ...) puedan obtener un token Bearer en primer lugar.

Por qué existe MAGICMARKETS_FIREBASE_WEB_API_KEY: el salto 2 anterior es una llamada a la API de Identity Toolkit de Firebase, no a ningún endpoint de Magic Markets, y Firebase requiere una clave de API web — limitada al proyecto Firebase de Magic Markets — para identificar qué token personalizado del proyecto se está canjeando. No es un secreto acuñado por llamador; es la misma clave a nivel de proyecto que cualquier cliente web de Firebase ya incrusta en el lado del cliente. No tiene un valor predeterminado seguro porque es específica del proyecto y del entorno (STG y producción son proyectos Firebase diferentes), así que — como MAGICMARKETS_SESSION_GROUP_ID — debe establecerse explícitamente dondequiera que se espere un llamador OAuth Bearer, o cada llamada autenticada con Bearer fallará.

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

Limitaciones conocidas

  • Este proceso canjea un token personalizado de Firebase por un token de ID por sí mismo, en lugar de que eso sea problema de Magic Markets. POST /oauth2/firebase-token podría igualmente llamar a signInWithCustomToken en el lado del servidor y devolver un token de ID listo para usar — ahorrando a cada implementador de servidor MCP (no solo a este) la necesidad de MAGICMARKETS_FIREBASE_WEB_API_KEY, una dependencia directa del endpoint de Identity Toolkit de Google, y el conocimiento de la distinción token personalizado/token de ID por completo. Esto es una solución del lado del cliente para una brecha en el contrato del Servidor de Autorización, no el estado final previsto — revisa una vez/si /oauth2/firebase-token devuelve un token de ID (o la API v2 acepta un token personalizado directamente).
  • La caché está en proceso, no compartida. La caché de MeResolver es un LRU por réplica (vía la variante expirable de hashicorp/golang-lru), no el estado sellado e independiente de réplica que internal/mcpserver/oauth.go usa para el estado del proxy OAuth. En un despliegue alojado con múltiples réplicas, una solicitud enrutada a un pod diferente que una anterior solo paga un intercambio extra (ahora dos llamadas: /oauth2/firebase-token y signInWithCustomToken) en un fallo de caché — no falla, a diferencia de un MAGICMARKETS_OAUTH_PROXY_SECRET no compartido. Esto es una solución deliberada, no el estado final: una caché compartida (o una credencial documentada y de mayor duración de Magic Markets) eliminaría por completo el costo de arranque en frío por réplica.
  • El TTL y el tamaño de la caché son constantes fijas, no variables de entorno (magicmarkets.MeCacheTTL = 1 minuto; un tamaño de 4096 tokens de acceso distintos) — ver internal/magicmarkets/meauth.go. Esto mantiene la solución simple mientras el contrato de /me aún se está consolidando; revisa una vez que valga la pena ajustarlo.
  • MAGICMARKETS_SESSION_GROUP_ID y MAGICMARKETS_FIREBASE_WEB_API_KEY no tienen un valor predeterminado seguro y difieren por entorno — ambos deben establecerse explícitamente dondequiera que se espere un llamador OAuth Bearer, o cada llamada autenticada con Bearer fallará.
  • Sin renovación proactiva de tokens. La resolución se reintenta en cada fallo de caché, pero nada renueva un token de acceso antes de que expire — un token expirado se manifiesta como un intercambio fallido (y por lo tanto una llamada de herramienta fallida), igual que cualquier otra credencial inválida.

Haciendo un cambio

Ver CONTRIBUTING.md para el flujo de trabajo rama → código → verificación → PR, incluyendo qué hacer si tu cambio toca la especificación OpenAPI vendida.


Licencia

MIT — ver LICENSE.

Mantenido por Magic Markets.