CardCore
Back to Blog

Building CardCore - April 15, 2026 - 50 Down, 500 to Go, and LXO Just Signed On

April 15, 202612 min readBy Dan Vohasek
building-in-publicstar-wars-unlimitedtournamenttrade-inpartnershipslxosector-qualifierpricingsecuritydeck-builder

OK. Deep breath.

It is Wednesday morning, April 15. Three days ago we ran CardCore live at a 50-player Premier Quest at TCF Gaming Arena. Three days from now we will run it at a 500-player Sector Qualifier at Gas South Arena.

In between: the most productive, slightly panicked, caffeine-fueled stretch of building I have ever done on this project. I am writing this at my kitchen table with a coffee that is the exact temperature of "I stopped paying attention to it" and I want to tell you the story of the last 72 hours because it is wild.

Short version: the PQ was a hit. We made real money. We made real trades. We made at least one trade I still think about every time I close my eyes, and that trade rewrote an entire subsystem of CardCore within 24 hours. We also signed our biggest partnership yet with a team of professional players who actually win these things. And Saturday we walk into a venue ten times bigger with a platform that has been hardened, polished, and frankly made much prettier in the last three days than it has been in the last three months.

Let's go.

The PQ was great. Mostly.

50 players. Full day of swiss, top cut, a crowded booth, a steady hum of people using phones while waiting for pairings. Revenue was strong. Trade-ins were strong. The Square Terminal did its job. The pull list did its job. Discord alerts fired for every order. Staff handled walk-in trade-ins without needing me to hover.

And then Sunday morning I sat down with a coffee and the numbers and realized we had paid $90 in credit for a card that was actively selling on the market for $60.

Allow me a moment of professional pride here: I am really good at building features. I am, it turns out, less good at anticipating every single way a pricing engine can be gamed by the natural shape of a market that has both "what the card sold for last month" and "what the card is selling for right now" and those two numbers are not the same.

So. We traded with love and paid with a stale ceiling. That one card cost us thirty bucks. Not catastrophic. But the lesson was: if this bug existed at a 50-person event, it existed times ten at a 500-person event. And I had three days.

Pricing, rebuilt from the inside out (COREAPP-1199, 1200)

Within 24 hours of that coffee I shipped what is easily the most important ticket of the week.

The technical name is "down-squeeze protection." The plain-English name is: if the current low price of a card is way below its market price, we assume the market price is stale and we pay based on the low. New helper function in the pricing library, threaded through price-check, trade-in submission, and staff add-to-trade flows. You physically cannot overpay on a squeezed card anymore. The system will not let you.

At the same time, the staff trade-in review UI got a full glow-up. Card images went from postage-stamp-sized to actually legible. Card names got their own full row with real weight. A new Stock column shows exact variant stock and total card stock side by side so staff know instantly whether they are buying a card they already have fifteen of. A new Low column lights up amber whenever the low price is pulling the reference down. And if the current staff offer exceeds the card's live low, the entire row turns red, an alert icon appears, and the unit offer text goes red. You cannot miss it. The UI screams at you. That is by design.

Then because new trade-in submissions sit in a queue, and by the time staff gets to them the market has moved, I shipped the Recalculate Prices button (COREAPP-1200). One click loops every line item through the full current-market pricing chain and shows you a toast that says "12 of 18 items updated, new total $247." Staff do not have to remember to re-check prices. They click a button. Done.

Two more trade-in follow-ups landed today: column casing fix on extended card numbers that was zeroing out some stock counts (37ecd0b), and the "Low" column now uses the 2-day minimum street price instead of the 3-month floor, so it actually reflects the current market (822d68b). Subtle, but the whole point of Low is "what is this card going for RIGHT NOW."

COREAPP-1201 - the audit pass I am genuinely proud of

While I was elbow-deep in the pricing engine I decided to do a full security and reliability pass on every mutation endpoint in the app. Because if pricing had a hole, maybe other things had holes too.

Nine findings. Three deploy batches. Zero actual customer damage (confirmed by forensics on the live data) but all nine were real bugs that were one busy day away from biting us.

Batch 1: value caps and store-ownership guards on every staff and customer mutation endpoint. Staff offers capped at $10,000. Counter offers capped at $10,000. Token grants capped at 500. Every endpoint now verifies the caller actually owns the store they are mutating. Hypothetical compromised staff account on Store A can no longer reach into Store B and start issuing credit. It was always theoretical, never exploited, and now it is also impossible.

Batch 2: extended the down-squeeze cap to the auto-buylist sync. Same bug class as the pricing fix, different code path. Feature is currently off. But I was not about to ship it with a known hole, so we fixed it now.

Batch 3 is the one I lose sleep over. Trade-in completion writes two side effects: a credit ledger row and an inventory acquisition row. Before this fix, two staff members clicking "complete" on the same trade at roughly the same time could race through the code and issue credit twice. Now SELECT ... FOR UPDATE locks the row, the writes are idempotent via ledger lookups, and if anything fails the whole thing throws a real 500 with a remediation message instead of silently swallowing the error. Same pattern applied to the order cancel credit path.

30 of 30 smoke tests passed across all three deploys. Zero new Sentry errors. This kind of work is invisible when it goes right. It is very, very visible when it goes wrong. On Saturday I want all of it to be invisible.

Meanwhile, at the deck builder (nine tickets in three days)

The deck builder has quietly become one of the most-used parts of CardCore, and over the last three days it has been rebuilt to match. A bunch of the work came from LUVR and Mo's feedback after the PQ - players giving us the list of "this looks cramped, this is hard to read, this needs to work on a tablet." So we did it.

The deck view page (COREAPP-1181, 1184, 1192) now leads with a proper leader-and-base hero right at the top, at the size it deserves. The stats sidebar is wider so the numbers have room to breathe. The visual view badge moved to the bottom center of every card, recolored to the site's gold, ring-highlighted so it pops. Every card now has a hover preview. The leader hero even has a flip button so you can see the back face of the leader card without leaving the page.

The deck editor (COREAPP-1180, 1183, 1188, 1193) got a complete rhythm change. Filters used to hide behind a toggle button. Now they are always on, always visible, in a compact reflowed layout. Resource Cost went multi-select so you can finally filter "cost 3 OR cost 5" instead of picking one. The editor layout went to a clean 50/50 split at larger sizes. Result tiles got bigger. The left-panel deck list got bigger tiles, bigger text, and a properly sized quantity column because reading "x3" on a phone at arm's length was not working. The toolbar split into "toggles" and "actions" with a visual divider so it stops looking like a wall of identical-looking buttons. Considerations got capped so it stays subservient instead of eating the whole sidebar.

The deck export (COREAPP-1185, 1187, 1189, 1190) got smaller quantity badges that hang cleanly off the bottom edge of each card image, the "Base //" suffix stripped from external exports, and the Considering zone excluded from all exports (Melee, SWUDB, Karabast, plain text, JSON). Nineteen new unit tests went in around the export formatter because this is the kind of code that is easy to get wrong silently.

And trade-in drafts (COREAPP-1191) are now server-side. You can start a trade-in on your phone at the shop, close the app, open it on your laptop at home, and it is there waiting for you. Auto-saves 2.5 seconds after you stop typing. Deletes itself on successful submit. New tradein_drafts table, full CRUD API, migration 116 landed on prod with zero drama.

The nav finally makes sense (COREAPP-1195-1198)

I owe a public apology to everyone who spent the last month trying to figure out whether "Collection" was under "My Cards" or "Trade Hub" and why they were different.

COREAPP-1197 consolidated Collection, Find Traders, My Trades, Markers, and Want Board into a single "My Card Hub" dropdown. One entry. Everything personal lives under it. Mobile has a matching "My Card Hub" button that opens a sheet with the same items.

COREAPP-1196 migrated the top ten user-facing pages to a wide layout with about 10 percent gutters at 1920 pixels. The content fills the screen now instead of hugging a 960-pixel column in the middle of a 27-inch monitor.

COREAPP-1198 added card search autocomplete directly in the nav. Debounced 250ms. Keyboard navigation. Click-outside close. Up to 8 results with image, name, set. "See all results" footer falls through to the full search page. Finally.

COREAPP-1195 removed the Sealed Product panel from the top of the Store Inventory page because, as it turns out, nobody scrolls through the cards looking for booster boxes. Sealed moved to its own dedicated place in the nav.

COREAPP-1186 rebuilt the homepage with responsive shelves that fill the width and actually use the real estate on a big screen. Trending Up and Trending Down got proper card-tile grids instead of text rows. Recent Activity and Upcoming Events sit side-by-side on large screens.

And COREAPP-1194 killed the ugly "unstyled HTML flash" that would sometimes show on mobile when searching the cards page. Route-level error boundary inside the root layout so Tailwind stays applied even when something goes wrong, and router navigations wrapped in React's startTransition so the current UI stays painted while the next page loads.

A lot of small things. All of them visible. All of them ones that players will quietly not notice on Saturday, which is exactly what I want.

LXO

OK. Big news.

This week CardCore signed a partnership with LXO, a professional Star Wars Unlimited team that won the last Sector Qualifier in Milwaukee.

Let me phrase that again for emphasis. The team that won the last SQ. Signed with CardCore.

They get visibility and tools built around how top players actually evaluate decks - The Lab, the deck editor, the meta overlay, all of it. We get their feedback, their content, and their presence at our events. When LXO tests a deck in The Lab, we learn things about what top players care about that I cannot get any other way. When LXO pilots a deck on stream, CardCore is right there on the export image with our logo in the corner.

This partnership is not just about Saturday. It is about the next twelve months of CardCore. Tools built for pros get used by everyone. Players across the country are going to start asking their local shop to use CardCore because that is what the players they look up to are using. That ripple is not visible yet. It will be.

A full rollout post on LXO is coming later this week with more details. For today I just want this on record: it happened. It is real. And it changes the shape of this entire project.

500 players. Saturday. Gas South.

Three days from now, CardCore runs live at a Sector Qualifier.

500 players. Gas South Arena, Duluth. Ten times the size of the PQ. Same me, same team, same platform, but in a venue that has a seating chart and a loading dock and a whole different class of "what if the network goes down."

The crunch week workstreams are almost all deployed. Infrastructure hardened (pool sizes, Sentry rates, Discord alerts, observability cron, pm2 logrotate). Person properties enriched for real analytics segmentation. Search fixes shipped (fuzzy matching, apostrophe handling, nicknames, did-you-mean, search-to-purchase attribution). Engagement engine live. Investor reporting dashboards going up this week. The pricing engine is bulletproof against the exact class of bug that cost us $30 at the PQ.

I will be at Gas South on Saturday running the booth. LXO will be there. Players will be walking up to the Square Terminal. Staff will be reviewing trade-ins with the new red-row warning. Somebody, somewhere, will try to do something we did not plan for, and we will fix it live because that is the deal.

What I actually want to say

You build for years and you do not know if it works. You ship to a small room and you find out.

The PQ was the small room. The SQ is the real room. In between was three days of sprinting, a partnership that changes the trajectory of the project, and a dozen quiet improvements that most players will never consciously notice but will definitely feel.

If you are coming to Gas South on Saturday, find the CardCore booth. Come say hi. Tell me what breaks. Tell me what you love. Tell me what is still missing. That is how this thing gets better.

Three days to go.

50 players showed us what breaks. 500 players will show us what scales. LXO showed us what this can be. Saturday we find out.