Now shipping 1.3.1

Walk into your next card show with a plan, not a hope.

mgz-pkmn turns a list of cards you want into a printable binder with thumbnails, market prices, and your negotiation floor. Built for collectors who'd rather show up informed than wing it on convention center Wi-Fi.

Everything you need at the table.

One pipeline, three open data sources, every output format that's useful for prepping a card show.

  • Multi-source pricing

    Layered lookup across pokemontcg.io (TCGPlayer + Cardmarket), TCGdex for international cards, and PriceCharting for region-exclusive products. Always negotiating from the same data the seller sees.

  • Built for want-lists

    Bulk syntax like top:5 Charizard cards or all Pikachu prints, inline price filters, language tokens. Describe what you're hunting, not every card name.

  • Negotiation comps

    Every row carries 80, 85, 90, and 95 percent of market. Your floor and ceiling are on the page when you sit down at the table.

  • See what you're hunting

    Thumbnails embedded directly in the spreadsheet and PDF binder. Recognize the right printing across a glass case at ten feet.

  • Print and go

    xlsx with totals and per-tag sections, PDF binder pages (3Γ—3 standard or 6Γ—4 condensed), set-completion checklist, structured JSON report. Take the formats you actually use.

  • Multilingual

    Japanese, Korean, Simplified and Traditional Chinese, German, French, Spanish, Italian, Portuguese, and more β€” automatic fallback when a card isn't indexed in English-only sources.

Three steps from idea to show floor.

Designed for the buyer's workflow first β€” but the same pipeline produces a seller's inventory sheet when you bring binders to the table.

  1. 01

    Write your want-list

    Plain text. One card per line. Use shortcuts like top 5 Mew | base set under $50 or all charizard cards | japanese β€” the parser does the rest.

  2. 02

    Run the tool

    pkmn lookup input.txt -o cards.xlsx --pdf binder.pdf --checklist checklist.pdf. Or open the web UI and paste your list β€” same pipeline, no install.

  3. 03

    Walk in informed

    Open the xlsx for live reference, hand the printable binder to the vendor, tick cards off the checklist as you find them. Negotiate from facts.

What you actually walk out with.

Every run produces the same three artifacts β€” pick the format that fits the moment. Screenshots below are rendered from the tracked sample outputs in output/.

  • Screenshot of the generated cards.xlsx workbook showing card rows with thumbnails, set, number, market price, and comparison columns.

    Spreadsheet

    cards.xlsx

    Per-tag sections, thumbnails inline, market price and your 80 / 85 / 90 / 95 % comps on every row.

  • Screenshot of a single page from the generated binder.pdf showing a 3Γ—3 grid of PokΓ©mon TCG cards with prices.

    Binder PDF

    binder.pdf

    Printable 3Γ—3 binder page β€” thumb, set, number, price. Hand it across the table.

  • Screenshot of a single page from the generated checklist.pdf listing cards by set with checkbox marks.

    Checklist PDF

    checklist.pdf

    Tick boxes by set, in collector-number order. Drop it in your binder and shop.

Newsletter

Get the next release in your inbox.

Occasional, no-pressure updates when a new version ships β€” and the odd Pokemon-TCG-meets-open-source note from @mgzwarrior. No spam, unsubscribe anytime.

Where it's going.

Roadmap organized by area β€” lookup, outputs, cache, web, devops. Speculative ideas live in the full roadmap; the committed work is below.

  • 1.3.1 β€” Shipped

    Hotfix to check that the release workflow now updates the marketing site as expected.

    View milestone β†’
  • 1.4 β€” In flight

    Was the v1.3 grab-bag before the pre-Scrydex catalog-warm epic took priority. Issues bumped from v1.3 land here; v1.3 is now focused on building a comprehensive English PokΓ©mon TCG cache against pokemontcg.io while it's still free.

    View milestone β†’
  • 2.0 β€” Planned

    Next major release. Deeper development per area: structured query DSL, eBay sold-listings, type-aware search, LRU cache, persistent run history, PyPI + Docker publishing, OpenAPI client codegen, mobile/a11y, and more. See docs/roadmap.md for the per-area breakdown.

    View milestone β†’

Full roadmap (including post-V2 monetization and V3+ proposals) lives in the repo.

Recently shipped.

The last few releases at a glance β€” straight from the changelog.

Full changelog β†’
  1. v1.3.1

    Fixed
    • Release: `rebuild-site` waits for the demo API to rotate before firing the Pages deploy hook (#399). Cutting v1.3.0 fired the hook ~5 seconds after the GitHub Release was cut β€” before Render had finished rolling out the new API β€” so Astro's build-time call to GET /api/v1/changelog baked the previous version into the static HTML and the hero pill stayed on Now shipping 1.2.0 until the hook was re-fired manually. release.yml now polls https://mgz-pkmn.onrender.com/version until it reports the new tag's version (15 s interval, 10 min budget) before firing the hook, with a warn-and-continue fall-through so a slow or stuck Render rollout never blocks the rebuild indefinitely.
  2. v1.3.0

    Added
    • API + CLI: Split lookup cache + stale-while-revalidate on pricing (#372). Phase 3 of the pre-Scrydex catalog-warm epic (#368) β€” the architectural shift that makes Phases 1 + 2's pre-warm pay off. The on-disk lookup cache now stores card payloads as two slices: structural fields (name, set, number, rarity, attacks, images, …) live under cache/api_structural/ with no TTL, while volatile pricing fields (tcgplayer.prices, cardmarket.prices, _pc_prices, _pc_url) live under cache/api_pricing/ with a 24 h TTL and stale-while-revalidate. Stale reads return the cached value immediately and spawn a background daemon thread that re-fetches upstream and writes a fresh pricing slice for the next request. Concurrent stale reads on the same key coalesce to a single refresh via a process-local in-flight set guarded by one threading.Lock. Legacy cache/api/{sha1}.json entries from before the split migrate lazily on first read β€” the legacy file is parsed, written to both new locations atomically, pricing mtime preserved via os.utime so a 9d-old legacy entry stays correctly STALE, and the legacy file is unlinked. /api/v1/lookup now advertises which path served the request through an `X-Cache` response header (HIT / STALE / MISS), closing the backend half of #310; /sets/{set_id}/cards + SPA chip are tracked as a tight follow-up. New CacheStats fields (api_structural_entry_count, api_structural_bytes, api_pricing_entry_count, api_pricing_bytes, api_pricing_oldest_mtime) surface in pkmn cache stats, GET /api/v1/cache/stats, and two new rows in the SettingsDrawer cache-stats panel. New ADR-0018 records the freshness-model decision; docs/cache.md documents the contract. Pinned by 20 new tests in tests/test_cache.py covering State A/B/C split reads, lazy migration with mtime inheritance, SWR coalescing, inflight cleanup on success/failure, and the X-Cache header on HIT / STALE / MISS for /api/v1/lookup.
    • API + CLI: Self-hosted per-card images via `pkmn cache warm-card-images` (#371). Phase 2 of the pre-Scrydex catalog-warm epic (#368). Three pieces wired together so the SPA gets self-hosted card images transparently: (1) a new pkmn cache warm-card-images subcommand with --sizes / --max-bytes / --skip-existing / --throttle-ms / --prefer-popular flags walks the catalog and downloads large and small image bytes into cache/images/cards/{size}/<card_id>.<ext> on the persistent disk (warmer in [src/mgz_pkmn/card_images.py](src/mgz_pkmn/card_images.py)); (2) a new GET /api/v1/cards/{card_id}/image/{size} route in [api/routes/cards.py](api/routes/cards.py) streams those files with a 30-day immutable browser-cache header, mirroring the existing get_set_logo route's 404-on-miss contract; (3) the lookup response and /sets/{id}/cards trim now rewrite images.{large,small} and thumb URLs to point at that route whenever the file is cached on disk β€” cache miss leaves the upstream pokemontcg.io URL in place so cold deploys still serve a working <img>. A new MGZ_PKMN_WARM_CARD_IMAGES_ON_STARTUP=1 env var (default off, separate from the existing two warm flags) opts a deploy into the runtime image warm bootstrap. New CacheStats fields (card_images_warm_timestamp, card_images_warm_count, card_images_warm_bytes, card_images_warm_budget_reached) surface state in pkmn cache stats and GET /api/v1/cache/stats. Pinned by 25+ tests in tests/test_warm_card_images.py and tests/test_card_images_api.py.
    • Release + site: Marketing site auto-rebuilds after a release, and the "Where it's going." teaser is now milestone-driven (#362). release.yml now fires a Cloudflare Pages deploy hook (CF_PAGES_DEPLOY_HOOK) after the GitHub Release is cut, so the hero pill and roadmap teaser pick up the new version once the demo API has rotated rather than waiting for the next site/** push. [RoadmapTeaser.astro](site/src/components/RoadmapTeaser.astro) now build-time-fetches the repo's milestones via the GitHub API and renders the most-recently-closed milestone as Shipped, the open milestone with the soonest due date as In flight, and the next open milestone as Planned β€” body copy comes from each milestone's description, falling back to a generic per-state line when empty. The previous hard-coded cards remain as the fallback when the GitHub call fails, matching the changelog helper's fail-open pattern. The rebuild job is continue-on-error: true, so a missing or bouncing deploy hook never blocks the release.
    Fixed
    • Web: Cache-stats panel upcasts byte counts through GB / TB (#390). The formatBytes helper in [SettingsDrawer.tsx](web/src/components/SettingsDrawer.tsx) capped at MB, so once the per-card image warm (#371) landed ~17 GB on disk the *Images* row read as 17073.0 MB instead of 16.7 GB. Function now mirrors the CLI's _format_bytes (powers-of-1024, B / KB / MB / GB / TB, one decimal once we leave the B range). Same pass renames the *Overrides* row to *URL overrides* to match the CLI label β€” the row tracks sticky (name, set_hint) β†’ PriceCharting URL entries from url_overrides.json, not a generic override bucket. Pinned by a new test in [SettingsDrawer.test.tsx](web/src/components/SettingsDrawer.test.tsx) asserting B / KB / GB upcasting against the deployed instance's 17 GB image-warm result.
    • Docs: `docs/cache.md` documents `pkmn cache warm-card-images` (#390). The warm-passes table and env-variable reference were both missing Phase 2 of the catalog-warm epic (#371). Added rows for the warm-card-images command and the MGZ_PKMN_WARM_CARD_IMAGES_ON_STARTUP env var, plus the deployed-instance final result (40088 images warmed (17904440414 bytes) across 173 sets) as a planning reference for disk size and first-deploy duration.
    • Deploy: `render.yaml` re-enables PR preview environments (#389). The blueprint had no previews block, so each sync against Render reverted preview generation to off and pull requests stopped getting their own preview deploys. Added a top-level previews: generation: automatic so every PR against main now spins a preview environment automatically (see Render's Blueprint spec).
    +4 more in the changelog β†’
    Changed
    • Deploy: Phase 1 catalog warm now enabled by default in `render.yaml` β€” MGZ_PKMN_WARM_CARDS_ON_STARTUP=1 joins the three already-enabled warm flags. First boot after this lands runs a full ~30 min background pass writing ~18,000 cache entries; every subsequent boot hits the 1-week card_warm.json freshness gate and skips. With persistent disk in place this is the bake-once, serve-forever shape the pre-Scrydex catalog-warm epic #368 is built around. Originally added as opt-in in #377 then promoted to default in #378.
    Added
    • CLI / API / web: `pkmn cache warm-cards` β€” pre-warms the per-card structural cache for the entire English PokΓ©mon TCG catalog (#370, Phase 1 of epic #368). Walks every set, then fan-out-writes a per-card cache entry for every card in the set's payload using a synthesized /v2/cards/{card_id} URL key β€” reuses the data each set's search already returns, so zero extra HTTP calls vs warm-set-cards. Flags for --set (repeatable), --max-cards (incremental warming), --skip-existing/--no-skip-existing (re-run is cheap by default), --throttle-ms (polite pacing against pokemontcg.io's rate limit), and -v. Writes a new card_warm.json manifest (1-week stale window) so subsequent runs and the runtime startup bootstrap can skip a recent pass. New card_warm_* fields on CacheStats surfaced on pkmn cache stats, /api/v1/cache/stats, and the SPA Cache Stats panel.
    • API: new `MGZ_PKMN_WARM_CARDS_ON_STARTUP` env var enables the per-card warm in the lifespan bootstrap. Independent of MGZ_PKMN_WARM_ON_STARTUP because the per-card pass is heavyweight (~18,000 cache entries on a fresh disk) and should be opted into explicitly.
    • API: `GET /api/v1/cache/stats` returns the same JSON shape as pkmn cache stats --json so operators can introspect a deployed instance's cache state without shelling onto the host (#311). Answers the "did MGZ_PKMN_WARM_ON_STARTUP actually land?" question on demand rather than from log-grep, with no auth required (entry counts and timestamps aren't sensitive) and Cache-Control: no-store so the reading reflects current on-disk state. Falls back to a zeroed snapshot on OSError (read-only / misconfigured filesystem) so the diagnostics endpoint never 500s on the surface meant to diagnose failures. Wired into [docs/deployment.md](docs/deployment.md#inspecting-deployed-cache-state) as the canonical inspection surface.
    +1 more in the changelog β†’
    Changed
    • Deploy: Persistent disk + runtime-only cache warming β€” the Render deployment now provisions a 10 GB persistent disk mounted at /var/cache and points XDG_CACHE_HOME at it (#369). The cache root resolves to /var/cache/mgz-pkmn, so every slice (API responses, set images, card images, URL overrides, the run-history SQLite file, all three warm-pass manifests) now survives redeploys instead of being thrown away on every push to main. The Dockerfile's build-time pkmn cache warm-sets step is retired in favor of a runtime lifespan bootstrap (_warm_sets_in_background) gated by a new sets_warm.json freshness manifest (1-week TTL). Image is ~20 MB smaller and builds ~30s faster as a result; a single warm pass after the first deploy now serves every subsequent deploy until the manifest expires. Foundation for the pre-Scrydex catalog-warm epic #368.
    Added
    • CLI / API / web: **sets_warm.json manifest + new sets_warm_* fields** on CacheStats. Surfaced as a new line on [pkmn cache stats](src/mgz_pkmn/cli.py) ("Sets: 173 sets Β· warmed Xh ago" or "not warmed"), a new row on the SPA's Cache Stats panel ([web/src/components/SettingsDrawer.tsx](web/src/components/SettingsDrawer.tsx)), and two new fields on GET /api/v1/cache/stats. Operators now have the same freshness signal for the set-image slice that the concept and set-cards slices already had.
    Fixed
    • API: `MGZ_PKMN_WARM_ON_STARTUP=1` actually fires again β€” the warm bootstrap was wired via @app.on_event("startup"), but Starlette silently drops on_event handlers when a custom lifespan is provided (the one added for Alembic auto-migrate). The deployed instance was reporting concept_warm_timestamp and set_cards_warm_timestamp as null on /api/v1/cache/stats despite the env var being set (#367). Folded the warm bootstrap into the existing lifespan async generator and pinned the behavior with tests/test_warm_on_startup.py so the next person to add a startup hook can't silently shadow it again.
    • Web: results table counts now live above the table β€” the N matched Β· N unmatched Β· N shown summary moved from below the results to the right side of the table toolbar so it's visible without scrolling on long result sets (#358).
  3. v1.2.0

    Changed
    • Repo: single source of truth for the brand logo β€” the tropical card-and-palm SVGs (light + dark) live once at [assets/logo.svg](assets/logo.svg) and [assets/logo-dark.svg](assets/logo-dark.svg). The marketing site (Header.astro, Footer.astro) and the demo SPA ([App.tsx](web/src/App.tsx)) pull them in via relative Vite imports β€” Astro uses ?url because its asset pipeline otherwise picks SVGs up as components; the SPA's bare import returns the URL string directly. Each surface's bundler still emits a hashed asset URL. Both Vite configs opt the dev server's fs.allow to include ../assets so the import resolves at dev time, and the Dockerfile's web-builder stage copies assets/ so the import resolves at production build time too. Drops the five prior duplicates (assets/logo-tropical.svg, site/public/logo-tropical{,-dark}.svg, web/src/assets/logo-tropical{,-dark}.svg); a logo change is now one file edit instead of a six-file sweep. See [ADR-0011](docs/adr/0011-marketing-site-stack.md#decision) for the updated rationale.
    • Web: Tropical palette across the SPA + theme toggle β€” the React demo SPA now ships the same husk/sand/sun/palm/coconut design system the marketing site uses, with a header Light/dark toggle that mirrors the site's behavior (persists in localStorage, follows OS prefers-color-scheme on first visit, no flash thanks to a pre-paint script in index.html). Light is the default to match the marketing site. Every component moves onto paired light/dark tokens β€” including the easter-egg modal, announcement banner, processing-queue stage chips, modals/drawers, results table, and over-cap / error / success accents (sun / ember / palm). Brand chrome uses the same tropical logo (sand-50 wordmark) in dark mode. SPA functionality is unchanged.
    • Web: Per-stage colors moved to the design system β€” the bulk lookup progress chips and the Loader2 spinner now use paired light/dark tokens (sky-500/sky-300 for looking_up, palm-600/palm-200 for resolved, sun-600/sun-300 for no_match, ember-500/ember-300 for error, etc.) instead of single Tailwind *-400 stock colors. Each pairing clears WCAG 2.1 AA contrast (β‰₯ 4.5:1) against both surfaces; the legend layout is unchanged. See [docs/accessibility.md](docs/accessibility.md).
    +2 more in the changelog β†’
    Added
    • Marketing: v1 interest survey + announcement banner β€” a slim dismissible top banner on the marketing site (above the Header) and the demo SPA (above the existing top bar) points visitors at a short Tally-hosted survey. ~6 questions covering pain points, useful features, return triggers, audience self-ID, favorite PokΓ©mon, and optional contact email. Source of truth for the question list lives at docs/marketing/surveys/v1-interest-survey.md; bump the survey URL and the survey-v1 dismissal-key suffix in both banner components together when shipping a future survey.
    • Site: print-ready show flyer β€” new /flyer page on the marketing site renders a double-sided quarter-letter (4.25 Γ— 5.5 in) handout for in-person card shows. Front: logo, tagline, and a high error-correction QR code pointing at the live demo. Back: four feature bullets and a contact block. A Download PDF (4-up) button generates a 2-page US Letter PDF with four flyers per sheet via jspdf + html2canvas, bypassing the browser print dialog so the saved file is always the right shape regardless of printer driver quirks. The @page print stylesheet remains as a fallback for power users.
    • Site: email signup section β€” a new "Get the next release in your inbox" section on the marketing landing page collects subscribers via the Buttondown public embed endpoint. Sits right under "What you actually walk out with" so visitors who've already seen the value prop have an easy on-ramp. Honors the tropical palette in both light and dark mode. Submits inline via fetch with a success state ("Thanks β€” check your inbox") when JS is enabled, falling back to Buttondown's hosted popup when JS is off. No new runtime dependencies; no API key in the client.
    +20 more in the changelog β†’
    Fixed
    • Site: social preview now matches the tropical look β€” the Open Graph / Twitter card image (/social-preview-tropical.png) was still rendering the old dark zinc background and blue card outline from the pre-tropical era; it's been redrawn on the cream + sun + palm + coconut palette with the new card-and-palm logo, the current "Walk in with a plan, not a hope." headline, and the v1.2 shipping pill. Regenerable from [site/scripts/social-preview.svg](site/scripts/social-preview.svg) via rsvg-convert -w 1280 -h 640 site/scripts/social-preview.svg -o site/public/social-preview-tropical.png.
    • Repo: README logo now matches the rest of the brand β€” [assets/logo.svg](assets/logo.svg) is replaced with the tropical card-and-palm logo (previously only the marketing site + SPA surfaced it). Every reference that uses the canonical raw.githubusercontent.com/.../assets/logo.svg URL β€” the README header, the GitHub Discussion posts that open with the inline logo, the welcome-email drafts β€” picks up the new mark on cache refresh; no link changes needed. The viewBox is trimmed to 0 0 285 88 (was 0 0 360 88 with ~80px of empty right padding), and a new [assets/logo-dark.svg](assets/logo-dark.svg) swaps the wordmark fill to sand-50 for dark surfaces. The README header uses a <picture> element so the right variant is picked from the viewer's OS dark-mode preference.
    • Deploy: a transient pokemontcg.io timeout during the Docker build's pkmn cache warm-sets step no longer fails the whole deploy. The set catalog fetch now retries transient timeouts with backoff (matching the card-lookup path), and the build's warm step falls back to a cold cache on a sustained outage instead of exiting non-zero.