{# Link previews. Absent until now, which meant every paste of this URL into Reddit, Discord or a group chat rendered as a bare link — on a project whose entire distribution is people pasting the URL. The image has to be absolute: the crawler fetching it has no page to resolve a relative path against, and a relative one fails silently on every platform at once. #}

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

Scans in. Priced CSV out.

Drop in a folder of card scans, or a .zip of them. 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 %}

{# A still, not the animation it replaced. The animation was three megabytes of the page's three and a bit, it was the largest thing painted inside the fold, and it went stale the moment a screen it showed was redesigned — a visitor could read an old version number off it in the middle of the hero. A screenshot of the one screen that does the persuading costs about a fortieth of that and says the same thing, and the other screens are now shown where the text makes their claim rather than all at once, up front, in twenty seconds nobody has agreed to spend yet. #}
The review queue: each scanned card beside the catalogue card it matched, with a confidence score, the runners-up it beat, and buttons to confirm the match or correct it.
The review queue. Your scan on the left, what it matched on the right, and the runners-up it beat.

Four steps, one archive

01

Import

Images straight from a scanner, one card per file, loose or in a .zip. Filenames don't matter. Front of the card is enough.

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

{{ exporters | join(' or ') }}: a CSV you upload yourself. Nothing here touches a marketplace on your behalf.

Any flatbed will do, and so will a phone on a tripod: the matcher wants a readable front, not a studio. jpg, png, tif and webp, up to {{ "{:,}".format(max_images) }} scans and {{ max_mb }} MB per upload.

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.

{{ games | length }} games, out of the box

Every one of these is a catalogue the install fetches for itself, on your machine, from the source plugins that ship with it. Ingest only the games you sell: each one is a separate command, and nothing is downloaded until you ask for it.

Missing yours? A source plugin is one Python file with one method that yields rows. The one that covers all {{ games | length }} above is about two hundred lines, and it is the worked example.

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.

A single card's page: every copy held with its condition and what it cost, beside a chart of the card's market price over time.
One card, every copy you hold, and the price trend that only exists because your install has been recording it.

It ends in a file you upload

Select what you want to list, pick a channel, and download the CSV. There is no API client behind any of these names, no OAuth token held and no key stored: nothing here can list, reprice or sell anything on your behalf, because nothing here can reach a marketplace at all.

The listing screen: selected inventory rows with their asking prices, a channel picker, and a button that downloads the marketplace CSV.
{{ exporters | join(' and ') }} today. An exporter is a column mapping in a TOML file, so a new one is a file, not a release.

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.