AGPL-3.0 · SELF-HOSTABLE · YOUR SCANS STAY PUT

Scans in. Priced CSV out.

Drop in a .zip of card scans. Get back an identified, priced inventory and a CSV your marketplace already accepts.

{% if viewer %} Open your inventory {% elif multi_user %} {{ 'Create an account' if registration_open else 'Sign in' }} {% else %} Open the app {% endif %} Run it yourself

{% 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. #}
{# The phone copy first: sources are matched in order, so this has to sit above the full-frame one or the wide source always wins. It is the same crop the stylesheet applies below 640px, done once at record time — which makes it both smaller to fetch and sharper, since the browser is no longer magnifying a slice of a downscaled frame. #} Reviewing matched cards, confirming them into inventory, opening a card to see its price trend, and exporting the selection as a marketplace CSV
The whole loop — including fixing a match it got wrong — in under twenty seconds. No narration: there is nothing to listen to.

Four steps, one archive

01

Import

A .zip of images, one card per file. Filenames don't matter.

02

Match

Every image is encoded and searched against a catalogue of reference card images. Confident matches go straight through.

03

Review

Everything else waits in a queue with its runners-up and their scores, so you decide the close calls.

04

Export

A TCGplayer or eBay CSV you upload yourself. Nothing here touches a marketplace on your behalf.

Why there is a review queue

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. #}
One scan of Charizard 004/102 ? match market
{% if proof_ids[0] %}{% endif %} Base Set · Holofoil 93% match $855.52
{% if proof_ids[1] %}{% endif %} Base Set (Shadowless) · 1st Edition Holofoil 91% match $10,000.00

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.

It keeps up with the market

Priced per printing, not per card

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.

A price history you own

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.

One line per card

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.

Sold is a state, not a delete

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.

Or run the whole thing yourself

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

Bring your own catalogue

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.

Exporters are just files

A marketplace exporter is a TOML column mapping, not code. Anyone can read one in ten seconds and be certain it does nothing else.