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 has four main stages where things can go wrong: fetching the deck from Moxfield, syncing or reading the local Scryfall database, matching deck cards against that database, and scoring candidates. Most errors are either a transient network block from Moxfield’s anti-bot layer, a missing or stale database, or a scoring edge case for highly unique synergy cards. The issues below cover every failure mode observed in normal use, with the exact error message and the steps to resolve it.
Cause: Moxfield’s Cloudflare protection identified the request as automated and blocked it. The fetch_deck_raw() function in moxfield_client.py raises a MoxfieldError with the message:
Moxfield devolvió 403 (probable bloqueo anti-bot/Cloudflare). Probar con otro User-Agent antes de asumir que el endpoint murió.
Fix: Wait a few minutes and try again — Cloudflare blocks are usually temporary. If the 403 persists, open moxfield_client.py and update the User-Agent string in DEFAULT_HEADERS to match a current browser version:
DEFAULT_HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
    ),
    "Accept": "application/json",
}
Replace Chrome/124.0.0.0 with the latest stable Chrome version number. Do not assume the API endpoint itself has changed unless a 403 persists after trying multiple User-Agent strings.
Cause: The deck ID could not be resolved on Moxfield. fetch_deck_raw() raises:
Mazo no encontrado (404) para deck_id='...'.
¿El link es correcto y el mazo es público?
This happens for two reasons: the URL is wrong, or the deck is set to Private on Moxfield.Fix:
  1. Double-check the Moxfield URL. The deck ID is the alphanumeric segment at the end — for example, in https://moxfield.com/decks/AbCdEf123, the ID is AbCdEf123.
  2. Open the deck on Moxfield, go to deck settings, and confirm the visibility is set to Public. Private and unlisted decks cannot be fetched by the tool.
Cause: The local Scryfall database has never been created. The tool’s database layer looks for ./data/cards.db (or the path set in DATABASE_URL), but sync_scryfall.py has not been run yet so neither the data/ directory nor the database file exist.Fix: Run the sync script once before launching any interface:
python sync_scryfall.py
The script creates the data/ directory automatically, downloads Scryfall’s oracle-cards bulk data (~38 k cards), and populates the database. The full sync typically completes in under two minutes depending on your connection speed. After the first run, re-run the script once daily if you want up-to-date prices.
Cause: Scryfall changed the shape of their bulk-data API response. The get_bulk_download_url() function in sync_scryfall.py looks for a jsonl_download_uri key in the JSON returned by https://api.scryfall.com/bulk-data/oracle-cards:
url = info.get("jsonl_download_uri")
if not url:
    raise RuntimeError(
        f"El objeto de bulk-data no trae 'jsonl_download_uri' "
        f"(keys: {sorted(info.keys())}). "
        "Scryfall pudo haber cambiado el formato otra vez."
    )
The error message already prints the actual keys present in the response.Fix: Visit https://api.scryfall.com/bulk-data/oracle-cards in a browser and inspect the returned JSON to find the correct download URL key. Then update get_bulk_download_url() in sync_scryfall.py to use that key instead of jsonl_download_uri.
Cause: There are three independent reasons a card can come back with zero suggestions:
  • No price: The card has price_usd = NULL in the local database, so suggest_alternatives() returns [] immediately and the card appears in report["no_price"].
  • All candidates filtered out: Every card cheaper than the original fails the color identity check (bitwise AND mask), the type compatibility check, or the format legality check. This is common for mono-colored or colorless cards in a five-color deck where the original is a gold card.
  • All candidates already in the deck: The exclude_oracle_ids set passed to suggest_alternatives() contains every oracle ID already on the board, so all matching candidates are skipped. The card appears in report["no_suggestions"].
Fix: Check both report keys in the JSON output:
python main.py "https://moxfield.com/decks/YOUR-DECK-ID" --json | python -m json.tool | grep -A 10 '"no_price"'
For Simic (blue/green) commander decks, colorless artifact cards are always color-identity candidates. If those also appear in no_suggestions, the deck genuinely saturates the cheaper card pool for that type and there are no further options.
Cause: The scored card is a synergy-specific card with no evergreen Scryfall keywords (aristocrat triggers, group slug effects, combo pieces, etc.). The scoring formula is:
score = 3.0 × keyword_similarity
      + 2.5 × oracle_text_overlap
      + 1.0 × cmc_proximity
      + 1.0 × edhrec_popularity
When keywords is an empty list, keyword_similarity is 0.0. Highly distinctive oracle text means oracle_text_overlap is also near 0.0. The remaining two terms are capped at 1.0 each, making 1.5 roughly the ceiling for these cards even against a strong candidate.Fix: This is expected behavior, not a bug. Suggestions below 1.5 are still cheaper alternatives that passed the color identity, card type, and format legality filters — they share the same mechanical slot even if the rule text diverges. Evaluate them manually. The score thresholds from the README are a guide:
RangeInterpretation
3.0 or aboveStrong match — shared mechanics
1.5 – 3.0Moderate match — some functional overlap
Below 1.5Weak match — same type/color/format only
Cause: DATABASE_URL is set to a PostgreSQL connection string, but load_candidate_pool() in scoring.py explicitly raises NotImplementedError for any non-SQLite dialect because the _POOL_SQL query uses SQLite’s json_extract() function:
if engine.dialect.name != "sqlite":
    raise NotImplementedError(
        "El filtro de legalities usa json_extract de SQLite. Para "
        "Postgres hay que cambiar esa línea al operador jsonb '->>' "
        "antes de habilitar este dialect (el filtro de color_identity "
        "ya es portable, usa un AND bit a bit)."
    )
Fix: Update the _POOL_SQL query in scoring.py to use PostgreSQL’s jsonb ->> operator:
# Replace this (SQLite):
AND json_extract(legalities, '$.' || :format_key) = 'legal'

# With this (PostgreSQL):
AND legalities ->> :format_key = 'legal'
Then remove or adjust the NotImplementedError guard. The color identity filter (bitwise AND on color_identity_mask) is already database-agnostic and requires no changes.
Cause: A card name from the Moxfield deck has no matching row in the local database. The match_deck_cards() function in matching.py tries two lookup strategies — scryfall_id first, then case-insensitive name — and logs a warning for every card that fails both:
WARNING: No matcheó ninguna carta local para 'Card Name' (board=mainboard)
Common causes:
  • The local database is outdated and is missing a recently released card.
  • The card is a token or emblem, which are not included in Scryfall’s oracle-cards bulk dataset.
  • The Moxfield deck uses an alternate card name (split card half, flip card face, etc.) that differs from the oracle name stored locally.
Fix: Re-run python sync_scryfall.py to refresh the database with the latest Scryfall data. If the card is a token or emblem it will never appear in the oracle-cards dataset — this is a known limitation and the card will always remain in the unmatched list.

Build docs developers (and LLMs) love