# Soccer Fixtures, Sentiments & Odds Math (MCP tools as API) (`genkenobi/soccer-fixtures-odds-math`) Actor

One-call soccer toolkit: fixtures and results for any day (all comps), bet settlement vs real scorelines, de-vig (power), EV/minimum odds/Kelly pricing, parlay math. No API key. Each run executes one tool call, JSON to dataset.

- **URL**: https://apify.com/genkenobi/soccer-fixtures-odds-math.md
- **Developed by:** [Burhan Hayber](https://apify.com/genkenobi) (community)
- **Categories:** Sports, MCP servers, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 tool calls

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## soccer-mcp

An MCP server that gives AI agents **football fixtures, real results, bet settlement and honest odds
arithmetic** — one day, every competition, no API key.

Most football MCP servers wrap a paid data API and stop at "here is a list of matches". This one
ships the part that is usually missing: turning a price into a verdict. It de-vigs a market, compares
your probability against the offered odds, names the minimum price worth taking, sizes the stake and
settles the result against the real scoreline — including push and quarter-line handling that most
quick scripts get wrong.

### Tools

| Tool | What it answers |
|---|---|
| `get_fixtures(date, league?, only_finished?)` | Every football match on a day, all competitions, with scores once played |
| `get_results(date, league?)` | Finished matches with final score — the input for settling |
| `settle_picks(picks, date?)` | Settle a list of picks against real scorelines: per-pick result, hit rate, PnL, ROI |
| `devig_market(prices, method?)` | Strip the bookmaker margin and return the market's own probabilities (power method by default) |
| `evaluate_price(probability, odds, margin_pct?, kelly_fraction?, tax_pct?)` | Fair odds, EV, minimum odds, scaled Kelly stake, take-it verdict |
| `parlay_math(legs)` | Combined odds/EV of an accumulator and how fast the edge decays per leg |
| `get_team_sentiment(date?, teams?)` | **Team-Mood cards**: news sentiment per club — net score −1…+1, level (🔴/🟡/🟢), trend, hard absences, factor list with sources and sentiment deltas. Sentiment scores are model-assisted (LLM per-factor deltas) and weighted for freshness + source quality (unverified ×0.35); they are mood indicators, not betting advice. |
| `engine_status()` | Whether the optional private engine bridge is wired up |

### Arithmetic conventions

- **`ev()` reports `plausible`.** An edge above `MAX_PLAUSIBLE_EV` (+100 %) is arithmetically fine but
  almost certainly a broken price-to-line pairing: multi-line Asian handicaps and team totals map by
  opaque outcome ids, and a fair 98.5 % for "away +3" once met the price 26.0 of another line (+2461 %).
  Check the pairing before acting on such a number — never present it as value.

- **A fair value that equals your own model value is your model, not the market.** When the sharp line
  does not quote a market (BTTS, over/under, team totals) and the number is filled from a goal model
  fitted to 1X2, the two converge by construction. Treat it as model-based, and do not let a goal model
  lead on BTTS or over/under, where it is measurably over-confident.

- **De-vigging uses the power method** (`p_i = (1/o_i)^k`, `Σp_i = 1`), not a proportional split of
  the overround. Proportional de-vigging spreads the margin evenly and therefore overstates outsiders:
  on `[1.80, 3.50, 4.20]` the two methods differ by **1.5 percentage points** on the 4.20 — more than
  half of a typical 2 % value threshold. `devig_market(prices, method="proportional")` still returns
  the old numbers for comparison.

- **Bookmaker tax is a parameter, not an assumption**: `tax_pct` (Germany: 5.3 % at books that pass it
  on) comes off the payout, so it lowers `ev`, raises `minimum_odds` and shrinks the Kelly stake. All
  three arithmetic tools take it; default 0.

- **Settlement names** are historic: `1X2` means the **home win**, `2X2` the away win and `DC` home or
  draw. `X` (draw), `X2` (away or draw) and `12` (no draw) are also accepted, and so are the short
  codes another engine writes (`1`, `2`, `1X`, `12`, …) — they map onto the same markets, so a pick
  written elsewhere still settles instead of silently falling through.

### Install

```bash
pip install soccer-mcp          # or: uvx soccer-mcp
```

Run it as a stdio MCP server:

```bash
soccer-mcp
```

### Use with a client

Claude Desktop / Cursor / any MCP client (`mcp.json`):

```json
{
  "mcpServers": {
    "soccer": {
      "command": "uvx",
      "args": ["soccer-mcp"],
      "env": { "SOCCER_MCP_CACHE": "/tmp/soccer-mcp-cache" }
    }
  }
}
```

With Docker:

```json
{
  "mcpServers": {
    "soccer": { "command": "docker", "args": ["run", "-i", "--rm", "soccer-mcp"] }
  }
}
```

### Private tools (premium tier)

The public package stays keyless and free. A private deployment attaches its own tools — paid feeds,
sharp lines, model blending — through a plugin hook, so one server exposes both tiers:

```bash
SOCCER_MCP_PLUGINS=soccer_engine.mcp_tools,/opt/private/pro_tools.py soccer-mcp
```

Each plugin is a module (dotted path or file path) with a `register(server)` function that adds tools to
the same server. Nothing private enters this repository, and `engine_status()` reports what is loaded.
`SOCCER_ENGINE_PATH` optionally points at a private engine directory to bridge into.

### Environment

| Variable | Default | Meaning |
|---|---|---|
| `SOCCER_MCP_CACHE` | `~/.cache/soccer-mcp` | Where day scoreboards are cached |
| `SOCCER_MCP_TTL` | `900` | Cache seconds for the current day |
| `SOCCER_MCP_FINISHED_TTL` | `604800` | Cache seconds for past days (scores never change) |
| `SOCCER_ENGINE_PATH` | unset | Operator-only: path to a private analysis engine to bridge into |
| `SOCCER_MCP_PLUGINS` | unset | Comma-separated plugin modules (e.g. `soccer_mcp.plugins_sentiment`) |
| `SOCCER_SENTIMENT_URL` | unset | Team-Mood backend base URL (e.g. `https://…/soccer`) — enables `get_team_sentiment` |
| `SOCCER_SENTIMENT_TOKEN` | unset | Bearer token for the Team-Mood backend. On Apify: set this as a **secret** environment variable in the Console (Actor → Settings → Environment variables), not in the repo |

### Example

```
evaluate_price(probability=0.55, odds=1.95, margin_pct=3)
→ fair_odds 1.818, ev_pct +7.25, minimum_odds 1.873, take_it true, stake.scaled_kelly_pct 3.75

settle_picks(picks=[{"home": "VfB Stuttgart", "away": "Borussia Dortmund",
                     "market": "O2.5", "odds": 1.29, "date": "2026-09-18"}])
→ score "0:1", result "loss", profit -1.0, roi_pct -100.0
```

### Data source and limits

Fixtures and results come from ESPN's public day scoreboard (all competitions, one request per day,
cached). Requests carry **no custom User-Agent** — ESPN answers 403 to every custom UA. Date ranges
are rejected by that endpoint, so the server fetches by day.

Competition names are not part of the "all competitions" payload — each event only carries an ESPN
league id. The server therefore builds an `id -> name` map once (74 leagues, roughly 45 s, cached for
30 days via `SOCCER_MCP_LEAGUE_TTL`) and enriches each match with it. Competitions outside that map
keep an empty name, so every match also carries `league_id` and can still be grouped. The `league`
argument of `get_fixtures`/`get_results` is a case-insensitive substring filter: `"Bundesliga"`
matches the German **and** the Austrian one — pass `league_id` when you need certainty.

Know what this is not: the fixtures feed has no odds, no lineups and no xG. `get_fixtures` and
`get_results` are free public data; **the arithmetic tools take your own probability as input and
never invent one**. Nothing here promises profit: every model estimate is yours, and a positive
expected value on a handful of picks is noise.

### Development

```bash
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest -q                                   # odds arithmetic and settlement logic
python tests/smoke_stdio.py                 # end-to-end: real tool calls over stdio
docker build -t soccer-mcp . && \
  printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"c","version":"1"}}}\n' | docker run -i --rm soccer-mcp
```

Works on both MCP SDK generations: the server imports `MCPServer` (v2) and falls back to `FastMCP`
(v1), and every tool returns JSON text, which both versions hand to the client unchanged.

### License

MIT — see [LICENSE](LICENSE).

### Distribution

| Channel | Link |
|---|---|
| Smithery registry | [smithery.ai/servers/burhan-hayber/soccer-mcp](https://smithery.ai/servers/burhan-hayber/soccer-mcp) |
| MCP gateway URL | `https://mcp.smithery.ai/burhan-hayber/soccer-mcp` (Bearer Smithery key) |
| Self-host (public, key-free) | `https://vmd194739.contaboserver.net/mcp/soccer/mcp` |
| Apify (PPE, pay-per-call) | https://apify.com/genkenobi/soccer-fixtures-odds-math |

Install via Smithery clients: `npx -y @smithery/cli@latest install burhan-hayber/soccer-mcp --client claude`

# Actor input Schema

## `tool` (type: `string`):

Which soccer-mcp tool to run

## `arguments` (type: `object`):

Tool kwargs: get\_fixtures/get\_results {date, league?}, settle\_picks {picks:\[...], date?}, devig\_market {prices:\[...]}, evaluate\_price {probability, odds, margin\_pct?, kelly\_fraction?}, parlay\_math {legs:\[...]}, get\_team\_sentiment {date?, teams?: \["Bayern", "Union"]} — Team-Mood cards (news sentiment per team)

## Actor input object example

```json
{
  "tool": "get_fixtures",
  "arguments": {
    "date": "2099-01-01"
  }
}
```

# Actor output Schema

## `result` (type: `string`):

One dataset item: {tool, arguments, ok, data, duration\_ms, generated\_at}. data holds the tool's JSON result (fixtures, settlement, fair odds, Kelly sizing etc.).

## `runInfo` (type: `string`):

JSON object with run metadata: tool, ok, duration, generated\_at.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("genkenobi/soccer-fixtures-odds-math").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("genkenobi/soccer-fixtures-odds-math").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call genkenobi/soccer-fixtures-odds-math --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,genkenobi/soccer-fixtures-odds-math"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/hIOHXcckwuZJpWIyg/builds/STqslZpfgRziB4Xyo/openapi.json
