AGPL-3.0 · SELF-HOSTABLE · YOUR SCANS STAY PUT
Drop in a .zip of card scans. Get back an identified, priced
inventory and a CSV your marketplace already accepts.
{% if viewer %}Signed in as {{ viewer.email }}. {% elif multi_user %}Free while it is this young. Your scans, inventory and exports are yours alone — no other account can see them. {% else %}Your scans are processed on the host running this and are never sent anywhere else.{% endif %}
{# WebP first: the same run at 1600px and without the 256-colour banding, which is what makes it legible on a screen that paints two device pixels per CSS pixel. The GIF is the fallback and is 640px, so it is the one that looks soft — every browser has one, and almost none of them use it. #}
A .zip of images, one card per file. Filenames don't matter.
Every image is encoded and searched against a catalogue of reference card images. Confident matches go straight through.
Everything else waits in a queue with its runners-up and their scores, so you decide the close calls.
A TCGplayer or eBay CSV you upload yourself. Nothing here touches a marketplace on your behalf.
Reprints share artwork. Image search gives you a name you can trust and a printing you cannot — the same illustration appears in set after set, separated on the card by a symbol a few pixels wide.
{# The confidence bar is the app's own — same markup, same classes, same integer rounding as the review queue renders. Two bars this close beside that price gap is the argument, and it makes it faster than the numbers did. #}Both prices are real rows from the catalogue foilstack fetched, and the bars are the review queue's own. The two scores are illustrative — a scan of your card produces its own.
The same illustration, printed twice, separated by a small black symbol and the word "Shadowless" — and by eleven and a half thousand dollars. A tool that shows you only the winner will eventually sell that card for $855. So auto-accept requires a high score, a clear margin over the runner-up, and — when the runner-up is the same card in another printing — prices close enough that being wrong doesn't matter.
A foil is priced as a foil. Where several printings all answer to
"foil", you name the one you hold and the price follows it — until you
do, the dearest match is used and the row says guessed,
because an overpriced card sits unsold where an underpriced one is
already gone.
Prices sync daily and every move is recorded, so each card carries its own trend rather than a number with no past. Upstream keeps only today, so this is history that exists because you ran it.
Four copies of the same card is one row with a quantity, not four rows to scroll past. Open it to see every copy, its condition, what you paid and what it is worth now.
Mark a card sold and it leaves your stock totals while staying in the record with what it fetched, so the margin column means something.
Three containers and a Postgres volume. No telemetry, no upload of your images to anyone, and — running it yourself — no account to create: one implicit owner holds everything, and the login screen never appears.
git clone https://github.com/foilstack/foilstack.git
cd foilstack && cp .env.example .env
docker compose up -d # migrations run on start
docker compose exec web foilstack ingest --source tcgcsv --game pokemon
docker compose exec web foilstack embed
Source plugins fetch card data from upstream on your machine — this project redistributes no prices and no images. Adding a game or a sport means writing one plugin.
A marketplace exporter is a TOML column mapping, not code. Anyone can read one in ten seconds and be certain it does nothing else.