Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/invvd/mtg-cheaper-deck/llms.txt

Use this file to discover all available pages before exploring further.

MTG Cheaper Deck is a fan project built for local, single-user analysis. It is not a production service — it intentionally trades robustness and scalability for simplicity and zero infrastructure requirements. The limitations below are either deliberate design trade-offs or known gaps that are out of scope for the project’s goals. Understanding them helps you use the tool confidently and avoid surprises.

Unofficial Moxfield API

The tool fetches decks from the undocumented endpoint api2.moxfield.com/v2/decks/all/{deck_id}. Moxfield has no public API, so this endpoint is not officially supported and carries no stability guarantee. Moxfield can change the response shape, rename fields, or block requests at any time. When this happens, moxfield_client.py raises a MoxfieldError and all deck-fetching stops until the file is updated to match the new shape. The client is intentionally isolated from the rest of the pipeline so that failures are contained there and don’t cascade into the scoring or database layers.

Prices Are Not Real-Time

Card prices come from Scryfall’s daily oracle-cards bulk data snapshot — a static file downloaded when you run sync_scryfall.py. The database reflects prices from that last sync run, not the live market. A recently reprinted card, a card that spiked due to a spoiler, or a newly released set will show stale prices until you re-run the sync. For best results, schedule sync_scryfall.py to run once daily:
# Example cron entry (runs at 3 AM every day)
0 3 * * * /path/to/.venv/bin/python /path/to/sync_scryfall.py

Single-User, Local-Only Design

The Flask web app (webapp.py) holds analysis state in memory with no user sessions, no authentication, and no request isolation. If two users submit different deck URLs simultaneously, their results will overwrite each other. The app is designed to be run locally — http://127.0.0.1:5000 — by a single person at a time. Do not expose it to the internet or a shared network without first adding proper session management (e.g., Flask sessions keyed by a UUID) and authentication.

SQLite-Only Legality Filtering

The candidate pool query in scoring.py uses SQLite’s json_extract() function to filter cards by format legality:
SELECT *
FROM cards
WHERE price_usd IS NOT NULL
  AND json_extract(legalities, '$.' || :format_key) = 'legal'
ORDER BY price_usd ASC
This syntax does not work on PostgreSQL. Switching to PostgreSQL requires updating this query to use the jsonb ->> operator (legalities ->> :format_key = 'legal') and removing the NotImplementedError guard in load_candidate_pool(). The color identity filter is already database-agnostic — it uses a precalculated color_identity_mask integer column and bitwise AND, which works identically in both SQLite and PostgreSQL.

Oracle Text Scoring Is Language-Agnostic but Keyword-Dependent

The text tokenizer in scoring.py operates on English oracle text only. Non-English reprints or custom card proxies will produce poor scores because their rule text won’t overlap with the standard English oracle text stored in the database. Additionally, many synergy-focused cards (aristocrat payoffs, group slug enchantments, combo pieces) have no evergreen Scryfall keywords at all. For these cards, the keyword_similarity term in the scoring formula is permanently 0.0, and the total score is driven entirely by oracle text overlap, CMC proximity, and EDHREC rank. Scores below 1.5 are expected for highly unique cards — see the Troubleshooting page for guidance on interpreting weak matches.

Tokens, Emblems, and Basic Lands Are Not Scored

Scryfall’s oracle-cards bulk dataset contains one entry per unique Oracle ID and intentionally excludes tokens, emblems, and basic land variations. If your Moxfield deck references a token producer’s associated token card or an emblem by name, it will not be found in the local database and will appear in the unmatched section of the report. This is not a sync bug — these card types are simply absent from the data source the tool relies on.

No Support for Sideboard, Maybeboard, or Acquireboard

The _iter_board_entries() function in moxfield_client.py processes only three boards: commanders, mainboard, and companion. All other Moxfield boards — sideboard, maybeboard, acquireboard, and any custom boards — are silently ignored. Cards in those boards will not appear in suggestions or the report. If you want to analyze a sideboard, move those cards into the mainboard before submitting the deck URL.

EDHREC Rank Is Only Meaningful for Commander

The scoring formula includes an EDHREC popularity term with a weight of 1.0:
EDHREC_WEIGHT = 1.0

def _edhrec_score(edhrec_rank) -> float:
    if edhrec_rank is None:
        return 0.0
    return 1.0 / (1.0 + edhrec_rank / 1000.0)
EDHREC rank measures how widely a card is played across Commander decks. For Commander deck analysis this is a useful tiebreaker. For Standard, Modern, Pioneer, or other non-Commander formats, the EDHREC rank has no semantic connection to the format being analyzed — highly ranked Commander staples will receive a small boost in score even when they are not contextually relevant. This slightly biases results toward generic Commander-playable cards when analyzing non-Commander decks.
If you are hitting a runtime error rather than a design limitation, see the Troubleshooting page for specific error messages and step-by-step fixes.

Build docs developers (and LLMs) love