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.
- 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.
- 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.
- 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/.
-
Spreadsheet
cards.xlsxPer-tag sections, thumbnails inline, market price and your 80 / 85 / 90 / 95 % comps on every row.
-
Binder PDF
binder.pdfPrintable 3Γ3 binder page β thumb, set, number, price. Hand it across the table.
-
Checklist PDF
checklist.pdfTick 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.
Built in the open.
MIT-licensed, public roadmap, first-time contributors welcome. The same tool you're trying β shaped by anyone who wants to help.
-
Good first issues
A curated list of small, well-scoped tasks for first-time contributors. Labels for the area you want to touch β lookup, outputs, cache, web, site.
Browse starter issues β -
Discussions
Open questions, design proposals, and announcements live here. The right place to float an idea before it's an issue β or just say hi.
Open Discussions β -
Contributing guide
The full workflow from issue to merged PR: branch naming, sign-off, the local gate, PR conventions. Everything you need to land your first change.
Read the guide β
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.
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/changelogbaked the previous version into the static HTML and the hero pill stayed onNow shipping 1.2.0until the hook was re-fired manually.release.ymlnow pollshttps://mgz-pkmn.onrender.com/versionuntil 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.
- 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
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 undercache/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 onethreading.Lock. Legacycache/api/{sha1}.jsonentries from before the split migrate lazily on first read β the legacy file is parsed, written to both new locations atomically, pricing mtime preserved viaos.utimeso a 9d-old legacy entry stays correctly STALE, and the legacy file is unlinked./api/v1/lookupnow 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. NewCacheStatsfields (api_structural_entry_count,api_structural_bytes,api_pricing_entry_count,api_pricing_bytes,api_pricing_oldest_mtime) surface inpkmn 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.mddocuments the contract. Pinned by 20 new tests intests/test_cache.pycovering State A/B/C split reads, lazy migration with mtime inheritance, SWR coalescing, inflight cleanup on success/failure, and theX-Cacheheader 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-imagessubcommand with--sizes / --max-bytes / --skip-existing / --throttle-ms / --prefer-popularflags walks the catalog and downloadslargeandsmallimage bytes intocache/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 newGET /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 existingget_set_logoroute's 404-on-miss contract; (3) the lookup response and/sets/{id}/cardstrim now rewriteimages.{large,small}andthumbURLs 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 newMGZ_PKMN_WARM_CARD_IMAGES_ON_STARTUP=1env var (default off, separate from the existing two warm flags) opts a deploy into the runtime image warm bootstrap. NewCacheStatsfields (card_images_warm_timestamp,card_images_warm_count,card_images_warm_bytes,card_images_warm_budget_reached) surface state inpkmn cache statsandGET /api/v1/cache/stats. Pinned by 25+ tests intests/test_warm_card_images.pyandtests/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.ymlnow 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 nextsite/**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 iscontinue-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
formatByteshelper 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 as17073.0 MBinstead of16.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 URLentries fromurl_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-imagescommand and theMGZ_PKMN_WARM_CARD_IMAGES_ON_STARTUPenv 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
previewsblock, so each sync against Render reverted preview generation tooffand pull requests stopped getting their own preview deploys. Added a top-levelpreviews: generation: automaticso every PR againstmainnow spins a preview environment automatically (see Render's Blueprint spec).
Changed- Deploy: Phase 1 catalog warm now enabled by default in `render.yaml` β
MGZ_PKMN_WARM_CARDS_ON_STARTUP=1joins 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-weekcard_warm.jsonfreshness 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 vswarm-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 newcard_warm.jsonmanifest (1-week stale window) so subsequent runs and the runtime startup bootstrap can skip a recent pass. Newcard_warm_*fields onCacheStatssurfaced onpkmn 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_STARTUPbecause 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 --jsonso operators can introspect a deployed instance's cache state without shelling onto the host (#311). Answers the "didMGZ_PKMN_WARM_ON_STARTUPactually land?" question on demand rather than from log-grep, with no auth required (entry counts and timestamps aren't sensitive) andCache-Control: no-storeso the reading reflects current on-disk state. Falls back to a zeroed snapshot onOSError(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.
Changed- Deploy: Persistent disk + runtime-only cache warming β the Render deployment now provisions a 10 GB persistent disk mounted at
/var/cacheand pointsXDG_CACHE_HOMEat 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 tomain. The Dockerfile's build-timepkmn cache warm-setsstep is retired in favor of a runtime lifespan bootstrap (_warm_sets_in_background) gated by a newsets_warm.jsonfreshness 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.jsonmanifest + newsets_warm_*fields** onCacheStats. 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 onGET /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 dropson_eventhandlers when a customlifespanis provided (the one added for Alembic auto-migrate). The deployed instance was reportingconcept_warm_timestampandset_cards_warm_timestampasnullon/api/v1/cache/statsdespite the env var being set (#367). Folded the warm bootstrap into the existing lifespan async generator and pinned the behavior withtests/test_warm_on_startup.pyso 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 shownsummary moved from below the results to the right side of the table toolbar so it's visible without scrolling on long result sets (#358).
- 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
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?urlbecause 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'sfs.allowto include../assetsso the import resolves at dev time, and the Dockerfile's web-builder stage copiesassets/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 OSprefers-color-schemeon first visit, no flash thanks to a pre-paint script inindex.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-50wordmark) in dark mode. SPA functionality is unchanged. - Web: Per-stage colors moved to the design system β the bulk lookup progress chips and the
Loader2spinner now use paired light/dark tokens (sky-500/sky-300forlooking_up,palm-600/palm-200forresolved,sun-600/sun-300forno_match,ember-500/ember-300forerror, etc.) instead of single Tailwind*-400stock 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).
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 thesurvey-v1dismissal-key suffix in both banner components together when shipping a future survey. - Site: print-ready show flyer β new
/flyerpage 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 viajspdf+html2canvas, bypassing the browser print dialog so the saved file is always the right shape regardless of printer driver quirks. The@pageprint 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
fetchwith 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.
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) viarsvg-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 canonicalraw.githubusercontent.com/.../assets/logo.svgURL β 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 to0 0 285 88(was0 0 360 88with ~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.iotimeout during the Docker build'spkmn cache warm-setsstep 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.
- Repo: single source of truth for the brand logo β the tropical card-and-palm SVGs (light + dark) live once at [







