DeckFlow helps deck builders translate decks between Moxfield and Archidekt without manual editing. It also provides ChatGPT prompt-building workflows for single-deck analysis, cEDH meta-gap analysis, and head-to-head deck comparison, Commander Spellbook combo lookup, Scryfall card and mechanic references, an Ask-a-Judge handoff flow, public feedback capture, and a cache-backed category suggestion engine.
End-user documentation is served by the running web app at /help (feature guides) and /about (version, source, credits). This README keeps the developer-facing material (build, publish, API, CLI, deployment).
Repository description (≤350 characters): DeckFlow unifies Moxfield and Archidekt decks, harvests Archidekt category data, and exposes CLI/web tools for diffs, printing conflict reports, card/mechanic lookup, Ask-a-Judge handoff, feedback capture, and ChatGPT deck-analysis, cEDH meta-gap, and deck-comparison prompt generation with Scryfall and Commander Spellbook references.
A public Feedback form is linked in the site footer (/feedback). Submissions are stored through DeckFlow's relational storage provider. SQLite is the default and stores feedback.db at $MTG_DATA_DIR/feedback.db (falling back to ./artifacts/feedback.db in development). Postgres can be enabled with the database environment variables below.
An admin page at /Admin/Feedback displays submissions with filters for status and type, and lets you mark items Read, Archive, or Delete them.
Set these environment variables (via fly secrets set ... on Fly.io or the Render env var UI):
FEEDBACK_ADMIN_USER— basic auth username for/Admin/Feedback.FEEDBACK_ADMIN_PASSWORD— basic auth password.FEEDBACK_IP_SALT(optional) — salt for hashing submitter IPs. If unset, a random 32-byte salt is generated on first run and persisted in the feedback metadata table.
If FEEDBACK_ADMIN_USER or FEEDBACK_ADMIN_PASSWORD are not set, /Admin/Feedback returns 503 Service Unavailable. The public /feedback form continues to accept submissions.
Public submissions are rate-limited to 5 per hour per IP.
The feedback-submit rate-limit policy in DeckFlow.Web/Program.cs derives its
partition key from the CF-Connecting-IP request header (set by Cloudflare to
the originating client IP). The same helper, Program.DeriveCloudflareClientIp,
also drives the admin brute-force throttle — single source of truth for both
surfaces.
Spoofing X-Forwarded-For cannot rotate the partition key (the helper does not
read that header). The Phase 03 immediate-peer-IP shape (peer:<RemoteIpAddress>)
was rewritten in Phase 5 because Render's edge fans inbound traffic across
multiple proxy IPs, fragmenting per-client buckets — see Phase 5 Plan 05-02.
This trust-the-header model requires that the Render container origin be
reachable only via Cloudflare; otherwise CF-Connecting-IP is spoofable by a
direct-to-origin attacker. See "Admin throttle" below for the Render Inbound IP
Rules prerequisite — it covers both surfaces.
If CF-Connecting-IP is missing on a request, the partition falls back to
feedback:unknown (or admin:unknown for /Admin/* requests) and a warning is
logged. All unidentifiable traffic shares one bucket, fail-closed.
The /Admin/* routes (feedback console) are protected against basic-auth
brute-force by an application-layer throttle:
- Lockout window: 10 failed authentication attempts per client IP within a
15-minute fixed window. The 11th attempt returns
429 Too Many Requestswith aRetry-Afterheader value (seconds until window reset, in the range 1..900). - Persistence: the throttle state is stored in Postgres
(
admin_brute_force_bucketstable), so a deploy or container restart does NOT reset accumulated failure counts. There is no brute-force amnesty window on redeploy. - Client IP source: the throttle partitions on the
CF-Connecting-IPrequest header (same helper as the feedback rate-limit). Cloudflare always sets this to the originating client IP, so the partition key is stable per real client (not fragmented across the Render edge's multi-proxy IP fan-out). - Successful auth does NOT increment the bucket. Only
Challenge-emitted 401s (missing/malformed/invalid credentials) count toward the throttle.
The CF-Connecting-IP header is trusted only because Cloudflare proxies all
inbound traffic. To prevent an attacker from reaching Render's container origin
directly and supplying a fake CF-Connecting-IP header, configure Render Inbound IP Rules
to allow only Cloudflare's published CIDR ranges:
- Render docs: https://render.com/docs/inbound-ip-rules
- Cloudflare IPv4 CIDRs: https://www.cloudflare.com/ips-v4/
- Cloudflare IPv6 CIDRs: https://www.cloudflare.com/ips-v6/
Render dashboard: deckflow service → Settings → Inbound IP Rules → add the full Cloudflare list. Cloudflare publishes ~22 IPv4 + ~7 IPv6 CIDRs and announces changes on the same pages. Refresh the Render allow-list manually if Cloudflare publishes a CIDR change announcement.
Without this configuration, CF-Connecting-IP is spoofable by direct-to-origin
hits and the throttle can be evaded by rotating the header value per request.
- Both the admin throttle (
/Admin/*) and the feedback-submit rate-limiter (POST /feedback) read from the sameCF-Connecting-IP-derived partition function (Program.DeriveCloudflareClientIp), so the spoof-prevention requirement covers both surfaces. - The throttle table grows lazily — one row per distinct partition key. Stale
rows reset themselves on the next
RecordFailureAsyncafter their 15-minute window has elapsed. No periodic cleanup job is required.
Feedback and category knowledge/cache storage can use either SQLite or Postgres.
SQLite is the zero-config default:
- unset
DECKFLOW_DATABASE_PROVIDER, or setDECKFLOW_DATABASE_PROVIDER=Sqlite - optional
DECKFLOW_DATABASE_CONNECTION_STRING - if no SQLite connection string is set, DeckFlow stores
feedback.dbandcategory-knowledge.dbunderMTG_DATA_DIR, falling back to../artifacts
Postgres is intended for hosted deployments where local files should not be the source of truth:
DECKFLOW_DATABASE_PROVIDER=PostgresDECKFLOW_DATABASE_CONNECTION_STRING=<Postgres connection string>
DeckFlow creates its feedback and category/cache tables and indexes automatically on first use. You only need to provide the Postgres database, user, and connection string.
DECKFLOW_DATABASE_CONNECTION_STRING accepts either Npgsql key=value form (Host=...;Username=...;Password=...;Database=...) or a libpq URI (postgresql://user:pass@host:port/db, the default format Render and most managed Postgres providers hand out). URIs are normalized internally; URL-encoded passwords and ?sslmode=require query params are honored.
By default, dotnet test skips Postgres integration tests because they require Docker.
To run them:
- Ensure Docker (Desktop on Windows/macOS, daemon on Linux) is running and reachable from the test process. On WSL, enable Docker Desktop's WSL integration.
- Set the env var:
DECKFLOW_POSTGRES_TESTS=1 - Run:
dotnet test DeckFlow.Web.Tests/DeckFlow.Web.Tests.csproj --filter "FullyQualifiedName~PostgresStorageTests"
Testcontainers.PostgreSql will start a postgres:16-alpine container, run the tests against the live database, and dispose the container at the end.
DeckFlow.Corecontains parsers, diffing logic, exporters, and the Archidekt/Moxfield integrations.DeckFlow.Core.Loadingcentralizes deck input loading and Commander deck-size validation so the web app and CLI share the same parsing/import rules.DeckFlow.Webprovides an ASP.NET Core MVC UI for running syncs, ChatGPT prompt building, cEDH meta-gap analysis, deck comparison prompt building, card lookup, commander category browsing, and category suggestions.DeckFlow.CLIexposes deck comparison, category harvesting, and cache querying in a console tool.- The ChatGPT Analysis page is the primary single-deck analysis workflow: it resolves card text via Scryfall, looks up rules for mechanics via the WOTC rules page, queries Commander Spellbook for combos, and assembles a complete analysis prompt with reference data attached.
- The cEDH Meta Gap page compares a submitted deck against 1 to 3 EDH Top 16 reference lists for the same commander, resolves canonical card names through Scryfall, injects Commander Spellbook combo references, generates a structured
meta_gapChatGPT prompt, and renders the returned JSON as a readable upgrade path. - The Commander Categories page shows which Archidekt tags appear most often on decks where a given card is listed as commander.
- The DeckFlow Bridge browser extension lets DeckFlow fetch Moxfield decks from your logged-in browser session when the server path is blocked.
- DeckFlow can optionally prompt users to install the included DeckFlow Bridge extension when a Moxfield URL import would otherwise be blocked from the server.
- Restore/build:
dotnet build DeckFlow.sln - Run the web app:
dotnet run --project DeckFlow.Web - Use the CLI to compare or harvest decks:
dotnet run --project DeckFlow.CLI -- --help
scripts/run-web.sh— bash wrapper that rebuildsDeckFlow.Weband launches it onhttp://localhost:5173with no browser auto-launch.scripts/run-web.ps1— PowerShell equivalent for Windows terminals.
Browser-side scripts under DeckFlow.Web/wwwroot/ts/ compile to
DeckFlow.Web/wwwroot/js/ via the CompileTypeScriptAssets MSBuild target
(BeforeTargets="Build") in DeckFlow.Web.csproj. The compiled .js files
are NOT tracked in git — dotnet build regenerates them every time.
First-time setup on a new dev machine:
cd DeckFlow.Web
npm install typescript
This populates DeckFlow.Web/node_modules/typescript/ so the MSBuild target
can invoke node ./node_modules/typescript/bin/tsc -p tsconfig.json. The
Render production build does the equivalent in its Docker stage
(RUN npm install typescript), so deployments are unaffected.
If dotnet build DeckFlow.Web reports a missing tsc, run the
npm install typescript step above and rebuild.
DeckFlow.Web/wwwroot/css/site-common.csscontains shared shell and view-level styles that apply regardless of the selected color theme.DeckFlow.Web/wwwroot/css/site*.cssfiles remain responsible for theme palettes and component styling.DeckFlow.Web/wwwroot/css/site-mobile.cssloads after the active theme stylesheet to apply mobile-breakpoint overrides for selectors that themes redefine (e.g.,.back-to-top-button,.page-shell,.sync-column); cascade-safe mobile rules continue to live insite-common.css.- The theme picker now includes all ten two-color guild themes in addition to the existing wedges, shards, and specialty themes.
- Keep long-lived CSS out of Razor views; prefer shared stylesheets so caching and theme behavior stay predictable.
- Browser-facing JSON POST APIs now enforce same-origin
Origin/Refererchecks before processing deck sync, suggestion, mechanic lookup, and Archidekt cache-harvest requests. - The old sessionStorage page-snapshot restore path was removed. DeckFlow no longer writes
main.content-shell.innerHTMLinto storage or rehydrates raw HTML from storage on load. - These checks are meant to reduce cross-site request abuse and avoid re-inserting stale or storage-poisoned markup into the DOM.
- Publish the web app with
dotnet publish DeckFlow.Web/DeckFlow.Web.csproj /p:PublishProfile=IIS-LocalFolder - The publish output goes to
DeckFlow.Web/bin/Release/net10.0/publish/iis-local/ - The .NET SDK generates
web.configduring publish; there is no checked-inweb.config - In IIS, create an application such as
/deckflowthat points at that publish folder - Install the ASP.NET Core Hosting Bundle on the IIS machine
- The checked-in views and scripts are path-base safe, so links and API calls stay under the IIS application path instead of jumping to
/
- A
Dockerfile,fly.toml, andrender.yamlship at the repo root for one-command builds on Fly.io or Render. - For durable feedback and category cache storage without a persistent disk, configure Postgres with
DECKFLOW_DATABASE_PROVIDER=PostgresandDECKFLOW_DATABASE_CONNECTION_STRING=<Postgres connection string>. - If you keep the default SQLite provider in a cloud host, set
MTG_DATA_DIR=/dataand mount a persistent volume there sofeedback.dbandcategory-knowledge.dbsurvive deploys/restarts. - ChatGPT artifact folders are still filesystem-backed. Set
MTG_DATA_DIR=/dataand mount a persistent volume if saved ChatGPT sessions need to survive deploys/restarts. - The Dockerfile's entrypoint resolves
$PORTat container start so platforms that inject a dynamic port (Render) work without changes. - Moxfield URL caveat. Moxfield's Cloudflare edge blocks requests from datacenter IP ranges with HTTP 403/5xx. When that happens, DeckFlow automatically falls back to Commander Spellbook's public
card-list-from-urlendpoint (which accepts the same Moxfield URL) and loads the deck from there instead. The UI surfaces a warning banner noting that card printings, set codes, collector numbers, author tags/categories, and sideboard/maybeboard entries are not available through the fallback. For full metadata, users should copy the Moxfield deck export text and paste it into the deck input directly — that path continues to work from anywhere. - Optional browser-extension path. The web UI now detects Moxfield deck URLs before submit. If the optional DeckFlow Bridge extension is installed and the current DeckFlow origin is allowed in extension settings, the browser fetches the Moxfield deck directly and submits it through the existing form flow. If the extension is not installed, DeckFlow can prompt the user with the included install page (
/extension-install.html), which now serves a downloadable ZIP from/extensions/deckflow-bridge.zip. Browsers do not allow the site to silently install the extension. Mobile browsers are left on the normal server/fallback path and are not prompted for the extension. The Moxfield URL fields in the web UI also include a collapsible in-app hint that links to the install page and explains the allowed-origin setup.
- Extension folder:
browser-extensions/deckflow-bridge - Download/install page:
/extension-install.htmlserves/extensions/deckflow-bridge.zip - Current install mode: download ZIP, unzip it locally, then load unpacked via
chrome://extensionsoredge://extensions - Security default: the DeckFlow bridge only responds on origins the user explicitly allows in extension options
- The extension contains:
deckflow-bridge.jsfor the optional DeckFlow web-app bridgeoptions.html/options.jsfor managing the allowed DeckFlow origin listbackground.jsfor cross-origin Moxfield API requests
The ChatGPT Analysis page (/Deck/ChatGptPackets) guides you through a 5-step workflow. Step 2 generates the analysis prompt, Step 3 parses and renders the returned deck_profile JSON, Step 4 optionally generates a set-upgrade prompt using that parsed profile, and Step 5 parses and renders the returned set_upgrade_report JSON.
Three layouts are available via the toolbar: Guided, Focused, and Expert. They present the same underlying steps with different amounts of context and guidance text.
Choose an Input method (paste text or public deck URL) and provide either a Moxfield/Archidekt deck URL or pasted deck export text. The chosen mode round-trips with the form so it survives refreshes and workflow-step navigation. The service:
- Falls back to treating leading quantity-1 entries as the commander when no Commander section header is present (Moxfield plain-text exports), then validates the inferred commander against Scryfall before continuing.
- Rejects inferred commanders that are not legal by the workflow rules: legendary creature, legendary Vehicle, or a planeswalker whose oracle text says it can be your commander.
Configure the analysis:
| Setting | Purpose |
|---|---|
| Target Commander Bracket | Bracket 1–5. ChatGPT uses this when evaluating card quality, interaction density, and upgrade suggestions. |
| Analysis questions | Select one or more questions from the buckets below. |
| Card name | Required when card-specific questions are selected. |
| Budget amount | Required when the budget-upgrade question is selected. |
| Decklist export format | Moxfield or Archidekt — required when category questions are selected; optional for versioning questions. |
| Include card versions | When checked, the original deck's set code and collector number are sent so ChatGPT can preserve the exact printing for retained cards. |
| Preferred category names | Shown when Update categories is selected. One name per line; ChatGPT will prefer these over inventing new ones. |
| Protected cards | Cards that must appear in every generated deck version. |
Click Generate Analysis Packet to build the reference data and analysis prompt. The service:
- Resolves all deck cards via Scryfall (
POST /cards/collectionin batches of 75) to supply authoritative Oracle text. - Fetches official mechanic rules text from the WOTC rules page for any keyword mechanics found on resolved cards.
- Fetches the Commander banned list.
- Queries the Commander Spellbook API if combo questions are selected.
- Fires the banned-list fetch, set-packet fetch, and Spellbook combo lookup concurrently to minimize wait time.
- Generates a suggested ChatGPT conversation title displayed in the UI with a copy button.
The generated prompt uses ## section headings (TASK, EVIDENCE RULES, BRACKET GUIDANCE, ANALYSIS QUESTIONS, OUTPUT FORMAT, REFERENCE DATA, DECKLIST) to keep long prompts structured.
Paste the fenced deck_profile JSON block or raw JSON payload returned from ChatGPT. You can also paste a saved deck_profile JSON file here directly without filling out Steps 1 and 2 again. The page validates the payload, parses it into a strongly typed model, and renders a readable summary of:
- Format and commander
- Game plan, speed, primary axes, and synergy tags
- Strengths, weaknesses, deck needs, and weak slots
- Per-question answers with basis notes
- Full deck versions when versioning questions were requested
This step is local to the returned JSON. It does not regenerate the analysis packet or call upstream services again.
Select one or more recent MTG sets, or paste a condensed set packet override. The page generates a set-upgrade prompt that references the parsed deck profile and asks ChatGPT to evaluate new cards from each set as potential inclusions, with suggested cuts, bracket-fit notes, speculative tests, and traps called out per set. For Commander/precon-style sets (commander, duel_deck, starter), the packet is filtered to first-print cards only so reprints don't crowd out genuinely new candidates; standard expansions are unfiltered. The set dropdown loads asynchronously from /api/set-options so the page renders immediately. A deck in Step 1 is required; the parsed Step 3 deck profile is optional but strongly recommended — without it ChatGPT gets an empty schema and produces generic recommendations.
Paste the fenced set_upgrade_report JSON block or raw JSON payload returned from ChatGPT. The page validates the payload, parses it into a strongly typed model, and renders a readable summary of:
- Per-set panels: top adds with suggested cuts and reasoning, traps, and speculative tests
- Final shortlist broken into must-test, optional, and skip columns
Like Step 3, this step is local to the returned JSON. You can paste a saved set_upgrade_report JSON file here directly without re-running the earlier steps — Step 5 runs standalone when no deck source is present.
All ChatGPT prompts generated by this app (analysis, set-upgrade, deck comparison, meta-gap) explicitly instruct ChatGPT to return JSON inside a fenced ```json code block. Raw JSON outside a code block is rejected by the wording.
On the ChatGPT Analysis page, the Step 3 and Step 5 result panels include a Download session (.zip) button. The zip contains every artifact for the current run: the input summary, request context, prompts, schemas, and response JSON blobs. Files are stored only on your machine; no copy is retained server-side.
To resume a saved run later, expand Resume from a saved session (.zip) at the top of the form, choose the previously downloaded zip, and the page rehydrates the response JSON into Step 3 or Step 5. The browser's busy indicator runs while the upload is processed.
Zip contents:
- /chatgpt-packets:
00-input-summary.txt,01-request-context.txt,30-reference.txt,31-analysis-prompt.txt,41-deck-profile-schema.json,50-set-upgrade-prompt.txt,40-deck-profile.json,51-set-upgrade-response.json,all-prompts.txt,all-responses.txt
Re-import only consumes 40-deck-profile.json and 51-set-upgrade-response.json; the rest rides along for your records or future ChatGPT context.
Questions are grouped into collapsible buckets. Buckets with pre-selected questions open automatically on page load.
| Bucket | Notable questions |
|---|---|
| Core Deck Analysis | Strengths/weaknesses, win condition, consistency, power level, best meta |
| Deck Construction & Balance | Mana curve, lands and ramp, card draw, interaction count, underperformers |
| Strategy & Synergy | Key synergies, anti-synergies, commander support, protect-cards, game plan |
| Optimization & Upgrades | Cuts for strength, budget upgrades (requires amount), missing staples, faster/competitive, board-wipe resilience |
| Meta & Matchups | Performance vs. archetypes, pod weaknesses, tech options, hate pieces |
| Play Pattern & Decision Making | Ideal opening hand, tutor priorities, when to cast the commander, common misplays |
| Specific Card-Level Questions | Card worth including and better alternatives can each target multiple card names, and every [card] question is emitted once per card you add; also includes weakest card and too many high-CMC cards |
| Advanced / Expert-Level | Turn clock, disruption vulnerability, keepable hand percentage, redundancy, mana-base optimization |
| Combo Analysis (Commander Spellbook) | Combos already in the deck, combos one card away within the color identity — both use live Commander Spellbook API data injected into the prompt |
| Deck Versioning & Upgrade Paths | Bracket 2/3/4/5 version, 3 named upgrade paths, assign categories, update categories |
When any versioning or category question is selected, the analysis prompt instructs ChatGPT to:
- Output the full, complete 100-card decklist for each generated version — no truncation, no "fill with basics" shorthand.
- Count cards before responding to confirm the total reaches 100.
- Use the deck builder's inline format when an export format is chosen:
- Moxfield:
1 CardName (SET) collectorNumber— or with categories:1 CardName (SET) collectorNumber #Category1 #Category2 - Archidekt:
1 CardName (SET) collectorNumber [Category1,Category2]— commander line uses[Commander]
- Moxfield:
- Output a Cards Added and Cards Cut diff after each decklist, comparing against the original.
- Output a
deck_profileJSON block for each generated deck version. - When Include card versions is checked, preserve the original printing (set code + collector number) for every retained card.
- Assign categories — ChatGPT assigns functional role categories to every card in the deck. Plain text export is not supported; Moxfield or Archidekt format is required.
- Update categories — ChatGPT updates or reassigns categories using the preferred category names you provide. Preferred names are injected into the prompt; ChatGPT may add new categories only when none of the preferred names fit.
- Basic card types (Creature, Instant, Sorcery, Enchantment, Artifact, Planeswalker, Battle) are excluded as categories. ChatGPT is instructed to use functional role labels instead (Ramp, Card Draw, Removal, Wipe, Tutor, Win Condition, Protection, etc.).
- For category questions, the prompt explicitly requires the final decklist to be returned only inside a fenced
textcode block so it can be pasted directly into Moxfield or Archidekt bulk edit.
When either combo question is selected, the service calls the Commander Spellbook find-my-combos API before building the prompt:
- Returns up to 20 included combos (all pieces are in the deck) and up to 15 almost-included combos (exactly one card missing, within the deck's color identity).
- Each combo entry lists the card names, results, and up to 300 characters of instructions.
- Results are injected as a reference block in the prompt. ChatGPT is told to treat this data as authoritative.
- Results are cached for 30 minutes keyed by the sorted deck card list.
- API failures degrade gracefully — the analysis continues without combo data rather than failing.
The Deck Comparison page (/Deck/ChatGptDeckComparison) generates structured ChatGPT prompts for comparing two Commander decklists side by side. It lives under the ChatGPT dropdown alongside the Analysis page.
Paste two decklists (Moxfield/Archidekt URL or plain-text export) and select a Commander Bracket for each deck. Optionally name each deck — the service falls back to the commander name if left blank.
The service:
- Parses both decklists, resolving cards via Scryfall
POST /cards/collectionin batches of 75. - Falls back to per-card Scryfall search when a submitted name is an alternate-art or Universes Beyond printing that does not round-trip through the collection endpoint cleanly, then labels rendered decklists as
resolved name [printed as: submitted name]. - Queries Commander Spellbook for combos in each deck.
- Builds a comparison context document with bracket definitions, role counts (ramp, draw, interaction, wipes, recursion, closing power), mana curves, color identity, category overlap, and combo gaps.
- Generates a structured comparison prompt with
## TASK,## RULES,## COMPARISON AXES,## OUTPUT FORMAT, deck sections, and comparison context. The prompt instructs ChatGPT to produce both a human-readable comparison and a fencedjsonblock matching adeck_comparisonschema. - Generates a follow-up prompt for iterative refinement of the comparison.
Comparison axes include: commander role and game plan, speed and setup tempo, ramp, draw, spot interaction, sweepers, recursion, closing power (including combos), resilience, consistency, mana stability, commander dependence, table fit, major overlap/differences, and five concrete cards or packages that best explain the gap.
Paste ChatGPT's JSON response back into the form. The page parses the deck_comparison JSON and renders a formatted view with:
- Game plans and bracket labels for each deck
- Strengths and weaknesses per deck
- Key combos per deck
- Verdict panel: speed, resilience, interaction, mana consistency, closing power, and combo comparisons
- Shared themes and major differences
- Key gap cards or packages
- Recommended-for notes per deck
- Confidence notes (when ChatGPT flags uncertainty)
If you continue asking follow-up questions in the same ChatGPT thread, use 32-comparison-follow-up-prompt.txt to have ChatGPT revise the readable comparison and regenerate the full deck_comparison JSON block.
On the Deck Comparison page, the Step 3 result panel includes a Download comparison session (.zip) button. The zip contains every artifact for the current run: the input summary, both normalized decklists, combo summaries, context, prompts, schema, and response JSON. Files are stored only on your machine; no copy is retained server-side.
To resume a saved run later, expand Resume from a saved session (.zip) at the top of the form, choose the previously downloaded zip, and the page rehydrates the response JSON into Step 3. The browser's busy indicator runs while the upload is processed.
Zip contents:
- /chatgpt-deck-comparison:
00-comparison-input-summary.txt,10-deck-a-list.txt,11-deck-b-list.txt,12-deck-a-combos.txt,13-deck-b-combos.txt,20-comparison-context.txt,30-comparison-prompt.txt,31-comparison-schema.json,32-comparison-follow-up-prompt.txt,40-deck-comparison-response.json
Re-import only consumes 40-deck-comparison-response.json; the rest rides along for your records or future ChatGPT context.
The prompt-templates/deck-comparison/ directory contains reference templates for compact and JSON-structured comparison prompts: all-in-one, competitive meta, matchup, quick verdict, JSON matchup, JSON strict return, and JSON tuning variants. See docs/deck-comparison-prompt-cheat-sheet.md for usage guidance.
The cEDH Meta Gap page (/chatgpt-cedh-meta-gap) generates a structured ChatGPT workflow for comparing your deck against recent EDH Top 16 lists for the same commander.
Paste a public Moxfield or Archidekt URL, or paste deck export text directly. You can optionally override the commander name. The page then queries EDH Top 16 using:
- Time period
- Sort by (
TOPorNEW) - Minimum event size
- Maximum standing
The service parses the submitted deck, removes sideboard and maybeboard cards, resolves the commander, fetches matching EDH Top 16 entries, and sorts them newest-first before display.
Select 1 to 3 EDH Top 16 reference decks and generate the prompt. The service builds:
30-meta-gap-prompt.txt31-meta-gap-schema.json
While building the prompt, the service also:
- Resolves submitted-deck and reference-deck card names through Scryfall so alternate print names and reskins are converted to canonical Oracle names where possible.
- Normalizes split and multi-face names to the base/front name for prompt display.
- Queries Commander Spellbook for your deck and for each selected reference deck, then injects combo summaries into the prompt.
- Caps the reference-deck count at 3 to keep the prompt size reasonable once decklists and combo references are included.
The prompt is structured with clear sections:
ROLEEVIDENCE PRIORITYRULESINPUT DATAANALYSIS TASKOUTPUT CONTRACTJSON SHAPE
ChatGPT is instructed to:
- Write a concise human-readable meta-gap summary first.
- Then return a fenced
jsonblock whose top-level object ismeta_gap. - Prefer the supplied Commander Spellbook combo evidence over weaker inferred combo reads when they conflict.
- Fill every field, using empty strings, zero values,
false, or empty arrays when evidence is missing.
Paste the raw JSON or fenced json block back into the page. The shared JSON extractor accepts fenced responses and ignores surrounding prose or extra trailing fence noise before parsing the payload. The page renders:
- Overview and readiness score
- Win lines
- Interaction
- Speed
- Mana efficiency
- Core convergence
- Missing staples
- Potential cuts
- Top 10 adds and cuts
On the cEDH Meta Gap page, the Step 3 result panel includes a Download meta-gap session (.zip) button. The zip contains every artifact for the current run: the input summary, prompt, schema, and response JSON. Files are stored only on your machine; no copy is retained server-side.
To resume a saved run later, expand Resume from a saved session (.zip) at the top of the form, choose the previously downloaded zip, and the page rehydrates the response JSON into Step 3. The browser's busy indicator runs while the upload is processed.
Zip contents:
- /chatgpt-cedh-meta-gap:
00-input-summary.txt,30-meta-gap-prompt.txt,31-meta-gap-schema.json,40-meta-gap-response.json
Re-import only consumes 40-meta-gap-response.json; the rest rides along for your records or future ChatGPT context.
The Deck Sync page (/sync) compares two decks and generates the delta import needed to bring the target deck in line with the source.
Supported sync directions:
| Direction | Description |
|---|---|
| MoxfieldToArchidekt | Moxfield as source, Archidekt as target |
| ArchidektToMoxfield | Archidekt as source, Moxfield as target |
| MoxfieldToMoxfield | Compare two Moxfield decks |
| ArchidektToArchidekt | Compare two Archidekt decks |
For same-system comparisons, column labels update dynamically to reflect the source and target platform.
The Card Lookup page (/card-lookup) has two modes:
- Single Card (default; the only mode visible on mobile) — type a card name, get live Scryfall suggestions once you've entered 4+ characters, and picking a suggestion (or pressing Look Up) renders that card's Oracle text plus WOTC rulings inline via
GET /card-lookup/single. - Card List (desktop-only) — paste up to 100 card names and download the full Scryfall output as
.txt(POST /card-lookup/download) or structured.json(POST /card-lookup/download-json). The inline line editor with per-row autocomplete is still available for editing before downloading.
Under the hood all modes use the same ICardLookupService: the card collection is fetched via POST /cards/collection in batches of 75, and rulings are fetched per-card via GET /cards/{id}/rulings.
The Single Card result panel also detects keyword mechanics and ability words on the resolved card, looks up the current official WOTC rules text for each detected term, and renders those entries in a separate Keyword Rules panel below the card text. This is intentionally limited to Single Card mode so large list downloads do not fan out into extra mechanic-rule lookups.
The Single Card result panel includes an "Ask a rules question about this card →" link that deep-links into /judge-questions?card=<name>.
The Mechanic Rules page (/mechanic-lookup) looks up the current official Wizards Comprehensive Rules text for a keyword mechanic or rules term.
Behavior:
- Exact rules sections such as
Prowessreturn the matching numbered section and summary. - Glossary terms such as
Battleresolve through the glossary and, when the glossary points to a major rules section like310, the page now returns the full referenced section body rather than only the glossary sentence or section header. - The Clear button clears the saved input, summary block, and rendered rules text together.
The service caches the parsed Wizards rules document in memory for 6 hours so repeated lookups do not keep re-downloading the full rules text file.
The Ask a Judge page (/judge-questions) leads with a prominent link to the live community judge chat at chat.magicjudges.org/mtgrules — a 24/7 IRC channel (#magicjudges-rules on Libera.Chat) staffed by certified judges and rules experts. This is the authoritative path. When the page is opened with a ?card=<name> query parameter (e.g. from Card Lookup), it pre-formats a !CardName — opening message ready to copy into the chat.
A clearly labeled secondary ChatGPT prompt generator is provided below for casual play and quick second opinions. It carries a prominent disclaimer ("ChatGPT can be confidently wrong about MTG rules") and, if a reference card is supplied, fetches that card's Oracle text and rulings via GET /card-lookup/single and embeds them in the generated prompt. The prompt itself starts with the same warning so ChatGPT cannot bury it.
The Commander Categories page shows the Archidekt tags that appear most often on decks where a given card is listed as the commander. It reports what observers assigned, not what the app infers.
The AI Category Suggestions page supports multiple lookup modes:
CachedDataReferenceDeckScryfallTaggerAll
Current behavior:
ReferenceDeckreads exact categories from a supplied Archidekt deck URL or pasted Archidekt text.CachedDataruns a short local cache sweep, then reads category hits from the local Archidekt-backed store.ScryfallTaggerreturns oracle-tag style suggestions from Scryfall Tagger.Allcombines the cached-store path and tagger path, with EDHREC as a fallback when no other source returns anything.
The page also exposes a background Archidekt harvest button so the local category store can be refreshed while the rest of the UI remains usable.
- Run
dotnet run --project DeckFlow.CLI -- archidekt-cache --minutes 5to keep the local cache fed with the latest public decks. - The CLI runs a dedicated cache session that respects rate limits via Polly, records skips for noisy decks, and persists card/category observations to
artifacts/category-knowledge.db. - The web cache service reuses the same session logic for on-demand refreshes from the MVC UI.
- The AI Category Suggestions page can start a 5-minute Archidekt harvest as a background job. The rest of the site stays usable while it runs, only one harvest is allowed at a time, and a local browser notification/banner appears when the job completes.
- Background harvest state is polled from the web app, and the start button stays disabled while the job is queued or running.
- The cache session now stays alive for the requested harvest window even when the queue runs dry, and it retries transient recent-page fetch failures instead of ending the whole job early.
- Basic card type categories (Creature, Instant, Sorcery, Enchantment, Artifact, Planeswalker, Battle) are filtered out of cache suggestions.
Swagger UI is available at /swagger when running in Development mode.
POST /api/suggestions/card
Content-Type: application/json
{
"mode": "CachedData",
"archidektInputSource": "PublicUrl",
"archidektUrl": "",
"archidektText": "",
"cardName": "Guardian Project"
}
POST /api/suggestions/commander
Content-Type: application/json
{
"commanderName": "Bello, Bard of the Brambles"
}
Start a background harvest:
POST /api/archidekt-cache-jobs
Content-Type: application/json
{
"durationSeconds": 300
}
Poll a specific job:
GET /api/archidekt-cache-jobs/{jobId}
Get the currently active job, if any:
GET /api/archidekt-cache-jobs/active
curl -X POST http://localhost:5000/api/suggestions/card \
-H "Content-Type: application/json" \
-d '{"mode":"CachedData","archidektInputSource":"PublicUrl","cardName":"Guardian Project"}'
curl -X POST http://localhost:5000/api/suggestions/commander \
-H "Content-Type: application/json" \
-d '{"commanderName":"Bello, Bard of the Brambles"}'- Scryfall is used for card-name autocomplete, commander autocomplete, the Card Lookup page, card reference resolution in the ChatGPT Analysis workflow, and async set catalog loading.
- All Scryfall clients send a real
User-Agent, an explicitAcceptheader, and usehttps. - Card lookup uses
POST /cards/collectionin batches of 75 identifiers. - The Card Lookup page is capped at 100 non-empty input lines per submission (at most two
cards/collectionrequests plus onecards/{id}/rulingsrequest per unique resolved card, all throttled). - The ChatGPT workflow uses the same batch endpoint to resolve authoritative Oracle text for all deck cards.
- The set catalog is fetched via
GET /setsand cached in memory for 6 hours; the web UI loads it asynchronously via/api/set-options.
- Scryfall enforces a soft cap of 10 requests per second at the Cloudflare edge (no proactive
X-RateLimit-*headers on 200 responses; onlyRetry-Afteron 429). ChatGptDeckPacketServicethrottles all Scryfall calls to ~110ms apart (≈9 req/s) via a process-wide semaphore so batched collection lookups plus per-card fallback searches stay under the cap.- On a 429 the wrapper reads
Retry-Afterand retries once if the cooldown is ≤5 seconds; longer cooldowns surface as a friendly "Scryfall returned HTTP 429. Try again shortly." error instead of being misattributed to card/commander validation. - The CLI ships a diagnostic
scryfall-probecommand that calls Scryfall and dumps status, headers, and body — useful for reproducing rate-limit responses. Example:dotnet run --project DeckFlow.CLI -- scryfall-probe --endpoint random --repeat 25(intentionally triggers 429).
dotnet run --project DeckFlow.CLI -- compare \
--moxfield my.deck --archidekt other.deck --out diff.txt
dotnet run --project DeckFlow.CLI -- archidekt-cache --minutes 10
dotnet run --project DeckFlow.CLI -- category-find \
--card "Guardian Project" --cache-seconds 20The DeckFlow Bridge Chrome/Edge extension lets DeckFlow fetch Moxfield decks through your logged-in browser session when direct server-side requests fail.
See browser-extensions/deckflow-bridge/README.md for load-unpacked installation instructions, or open /extension-install.html in the running app to download the current ZIP package.
- Core logic is isolated in
DeckFlow.Core(diff engine, export helpers, parsers, integration clients, knowledge store). - Web and CLI layers orchestrate requests and rely on DI to resolve shared services.
- Importers for Archidekt and Moxfield implement typed interfaces (
IMoxfieldDeckImporter,IArchidektDeckImporter) for easy test substitution. ChatGptDeckPacketServiceparallelizes independent fetches (banned-list, set-packet, Commander Spellbook) usingTask.WhenAllto reduce total build time.ChatGptDeckComparisonServiceparses two decklists, resolves cards via Scryfall, queries Commander Spellbook for both decks, derives comparison context (role counts, mana curves, combo gaps), and generates structured ChatGPT prompts with a JSON output schema.CommanderSpellbookServicecaches results for 30 minutes and degrades gracefully on API failure.CategoryKnowledgeStorepersists observations through the configured relational provider. SQLite storesartifacts/category-knowledge.dbby default; Postgres can be selected withDECKFLOW_DATABASE_PROVIDER=Postgres.
- The floating back-to-top control uses inline SVG in the shared layout, not the old
chevron-up.pngbitmap. - The back-to-top button stays hidden while the page is already near the top and appears only after the user scrolls down.
A persistent theme picker in the shared layout lets users switch between visual themes. The selection is stored in localStorage and applied on page load. The shared layout now enhances that native select with an ARIA combobox button/listbox while preserving the original form control for form posts and keyboard fallback. Available themes:
- Default — the base site stylesheet
- Abzan (WBG), Bant (GWU), Esper (WUB), Grixis (UBR), Jeskai (URW), Jund (BRG), Mardu (RWB), Naya (RGW), Sultai (BGU), Temur (GUR) — color-shard/wedge-inspired palettes
- Nyx — enchantment-themed dark palette
- Planeswalker Dark — dark-mode palette
- Commander Table — warm tabletop-inspired palette