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.

After the hard filters narrow the candidate pool to cards that are cheaper, color-legal, and share a core type with the original, every surviving candidate receives a numerical similarity score. The scoring system uses explicit rules derived directly from Scryfall’s structured card data — keyword arrays, tokenized oracle text, converted mana cost, and EDHREC rank — rather than a machine-learning model. This keeps the tool fully deterministic, reproducible without a GPU, and easy to reason about when a suggestion looks surprising.

The Scoring Formula

Each candidate is evaluated against the original card using four weighted terms:
score = 3.0 × keyword_similarity
      + 2.5 × text_overlap
      + 1.0 × cmc_proximity
      + 1.0 × edhrec_popularity
The maximum theoretical score is 7.5 (identical keywords, identical oracle text, same CMC, and an EDHREC rank of 0). In practice, scores rarely approach that ceiling — a well-matched functional replacement typically scores in the 2.5–4.5 range.

Score Interpretation

ScoreMeaning
3.0 or higherStrong match — shares real mechanical function
1.5 to 3.0Moderate match — some overlap, not a clone
Below 1.5Weak match — mainly same type/color, popular on EDHREC
These thresholds are also used to color-code the terminal output: green for 3.0+, yellow for 1.5–3.0, and plain white for weaker matches.

Term 1: Keyword Similarity (weight 3.0)

Keyword similarity is the Jaccard similarity of the two cards’ Scryfall keywords arrays — the structured list of evergreen and deciduous mechanics such as Flying, Trample, Lifelink, Deathtouch, and Haste.
keyword_similarity = |keywords_A ∩ keywords_B| / |keywords_A ∪ keywords_B|
This term receives the highest weight because shared Scryfall keywords represent unambiguous, structured evidence of mechanical overlap that needs no text parsing.
Many synergy-driven archetypes — aristocrats, group slug, stax, control — have zero evergreen keywords. Their entire function lives in free-form oracle text. If keyword similarity were the only signal, the scorer would default to recommending generic staples that happen to share a type and a Scryfall keyword with the original, rather than cards that actually do the same job. The oracle text overlap term (Term 2) exists specifically to handle these cases.

Term 2: Oracle Text Overlap (weight 2.5)

Text overlap is the Jaccard similarity of the two cards’ tokenized oracle texts:
text_overlap = |tokens_A ∩ tokens_B| / |tokens_A ∪ tokens_B|
Tokenization transforms raw oracle text into a set of meaningful game terms:
  1. Lowercase the entire text.
  2. Strip reminder text in parentheses using the regex \([^)]*\).
  3. Extract all word tokens matching [a-z']+.
  4. Remove stopwords and words of two characters or fewer.
The stopword list covers common grammatical and game-rules filler words that carry no mechanical signal:
{"a", "an", "and", "any", "as", "at", "each", "for", "from", "if", "in",
 "into", "is", "it", "its", "may", "of", "on", "or", "target", "that",
 "the", "then", "this", "to", "up", "when", "whenever", "with", "you",
 "your", "have", "has", "are", "be", "by", "control", "controls", "put",
 "one", "another", "instead", "until", "end", "turn", "card", "cards"}
What remains after filtering is a set of game-meaningful terms: words like sacrifice, token, counter, draw, damage, graveyard, exile, library, and specific keyword actions (proliferate, convoke, cascade). Two cards that share a dense set of such terms are functionally related even if they share no evergreen keywords. These token sets are pre-computed for every card in the CandidatePool at load time, so the Jaccard calculation during scoring is a set intersection on already-prepared data.

Term 3: CMC Proximity (weight 1.0)

CMC proximity measures how close the candidate’s converted mana cost is to the original’s:
cmc_proximity = 1 / (1 + |orig_cmc - cand_cmc|)
The score is 1.0 when both cards have identical CMC, and it decreases as the difference grows:
CMC differenceScore
01.000
10.500
20.333
30.250
This term returns 0 if either card has no CMC value (e.g., certain land or token cards where the field is absent).

Term 4: EDHREC Popularity (weight 1.0)

EDHREC popularity acts as a tiebreaker and a gentle quality signal — a well-known, widely-played card is more likely to be a good suggestion than an obscure one with similar text:
edhrec_popularity = 1 / (1 + edhrec_rank / 1000)
EDHREC rank is a lower-is-more-popular integer (rank 1 is the most-played card in the format). Dividing by 1,000 scales the rank into a smooth curve where the top-ranked cards score near 1.0 and mid-tier cards score around 0.1–0.3. Cards with no EDHREC rank stored in the database return 0 for this term.

Hard Filters Applied Before Scoring

Three filters are applied to the candidate pool before any scoring takes place. A candidate that fails any filter is excluded entirely — it never receives a score.
  1. Pricecandidate.price_usd < original.price_usd (strictly cheaper; equal price is not accepted).
  2. Color identitycandidate.color_identity_mask & disallowed_mask == 0 (the candidate’s color identity must be a subset of the original’s; see the bitmask section below).
  3. Core type overlapextract_core_types(candidate) ∩ extract_core_types(original) ≠ ∅ (the candidate must share at least one core card type with the original).

Core Types Recognized

Type overlap is determined by extracting words before the separator in type_line that belong to the CORE_TYPES set:
CORE_TYPES = {
    "Artifact", "Battle", "Conspiracy", "Creature", "Dungeon",
    "Enchantment", "Instant", "Kindred", "Land", "Phenomenon",
    "Plane", "Planeswalker", "Scheme", "Sorcery", "Tribal", "Vanguard",
}
For example, "Legendary Creature — Elf Druid" yields {"Creature"}, while "Artifact Creature — Construct" yields {"Artifact", "Creature"}. A candidate Artifact Creature would pass the type filter for an original that is a plain Creature, because the intersection is non-empty.

Color Identity Bitmask

Color identity filtering uses a precomputed integer bitmask stored in the color_identity_mask column during Scryfall sync. Each color maps to a power of two:
ColorBit value
W (White)1
U (Blue)2
B (Black)4
R (Red)8
G (Green)16
A card’s mask is the bitwise OR of all its colors. Examples: a UB card has mask 6 (2 | 4); a Bant (WUG) card has mask 19 (1 | 2 | 16); a colorless card has mask 0. The filter computes the disallowed mask as the complement of the original’s mask within the five-color space, then checks that the candidate uses none of those disallowed colors:
disallowed_mask = 0b11111 & ~orig_mask
# passes if: candidate_mask & disallowed_mask == 0
This ensures that a candidate never introduces colors outside what the original card already requires — a critical constraint for Commander deckbuilding where the commander’s color identity defines what the deck is legally allowed to contain.

Build docs developers (and LLMs) love