{# 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
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 %}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. #}
Images straight from a scanner, one card per file, loose or in a
.zip. Filenames don't matter. Front of the card is enough.
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.
{{ 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.
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.
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.
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.
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.
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.