Foilstack
Open-source card scanning, inventory and listing export. Drop in a .zip of
card scans, get back an identified, priced inventory and a CSV your marketplace
will accept.
Runs on your own machine. Your scans never leave the host.
Status: early. The matching pipeline works end to end; expect rough edges.
Scans in, priced CSV out. Your scan on the left, the catalogue's guess beside it, and the score between them — because the top match is evidence, not an answer.
What it does
- Import — a
.zipof card images, one image per card, any filenames. - Match — every image is encoded and searched against a catalogue of reference card images. High-confidence matches are accepted automatically; everything else waits in a review queue with its runners-up.
- Inventory — condition, quantity, cost, market price, suggested list price, margin.
- Export — a CSV for TCGplayer or eBay, which you upload yourself.
Why the review queue matters
Reprints share artwork. A vector search over card images gives you a name you can trust and a printing you cannot — the same illustration may appear in a dozen sets at wildly different prices, separated on the physical card only by a set symbol a few pixels wide.
So the top match is shown next to its rivals with their scores, rather than
presented as an answer. The auto-accept threshold defaults to 0.92 and is
deliberately conservative: a missed auto-accept costs you one click, a wrong one
puts a real card on sale at another card's price.
When the catalogue is wrong about all of its guesses, search it by name and pick the right card yourself — from the queue before committing, or from the inventory row afterwards. You are holding the card and can read the name off it, which is information the encoder never had.
Quick start
git clone https://github.com/foilstack/foilstack.git
cd foilstack
cp .env.example .env
docker compose up -d
That starts Postgres, the encoder and the web app. Migrations run automatically
on every start, so an upgrade is git pull && docker compose up -d --build and
nothing else.
The default encoder is a gated model. Accept the terms for DINOv3 and put an
HF_TOKENin your.envbefore the first run, or the encoder fails to load with a 401 that looks like a network problem.
Then build a catalogue. Start small — one game, a few hundred cards — to check the whole path works before committing hours to encoding:
docker compose exec web foilstack ingest --source tcgcsv --game pokemon --limit 300
docker compose exec web foilstack embed
ingest pulls catalogue rows and image URLs; embed downloads each reference
image and encodes it. Two commands because they fail for different reasons and
take very different amounts of time — a network blip during encoding should not
cost you the ingest.
Ingest every game you intend to scan. Nearest-neighbour search can only
answer with a card that is in the catalogue, so scanning Magic against a
Pokemon-only catalogue returns Pokemon. foilstack plugins lists the games a
source can fetch.
Open http://localhost:8090.
Running it for other people
There is no login screen by default: one implicit owner holds everything, and
you never invent a password for a tool only you can reach. Set
FOILSTACK_MULTI_USER=true and a real secret key and every scan, job and
inventory row belongs to exactly one account — enforced by a NOT NULL column
and a test suite that drives the real app and asserts a stranger gets nothing.
docs/accounts.md — registration control, invite codes, storage quotas, sign-in rate limiting.
Backups
A service, not a cron job you have to remember. The backup sidecar starts with
everything else, dumps the database on a schedule, verifies what it wrote,
mirrors your scans beside it, and drops a BACKUP_FAILING file when a run
produces nothing usable. Checking for that file is the whole of your monitoring.
docs/backups.md — retention, copying off the machine, restoring.
Prices
Prices sync daily from TCGCSV and are stored per printing, so a foil is priced as a foil — which matters more than it sounds. Base Set Charizard is 1st Edition Holofoil at $10,000, Unlimited Holofoil at $2,146 and Holofoil at $855; ticking "foil" chooses between none of them.
Price history is the one thing here that cannot be rebuilt: upstream mirrors only the current day, so a day the sync does not run is a day of history gone for good.
docs/prices.md — the sync protocol, how history is stored, naming a printing.
Extending it
Source plugins fetch the catalogue and run code, so you add one deliberately and there is no registry. Export plugins are TOML column mappings — adding a marketplace means writing a file a reviewer can read in ten seconds and be certain does nothing else.
Encoding the catalogue
The encoder runs on CPU by default so the stack starts anywhere — about one card a second, which is more than a day for a full Magic catalogue. If you have an NVIDIA GPU, an overlay turns it on:
docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build
docs/encoder.md — picking the right CUDA wheels, checking which device it actually loaded on, and filling the catalogue.
This project redistributes no card data
The catalogue is fetched from upstream, on your machine, by a plugin you chose. We ship the code that knows how to ask — not the prices, and not the images.
The one exception is the animation above, which shows a handful of card thumbnails because a demo of a card tool with no cards in it would be useless. No card data ships in any form the application reads.
Contributing
AGENTS.md has the working notes: how the pieces fit, the conventions, and the handful of mistakes this codebase has already made once.
uv sync --extra dev
uv run pre-commit install --hook-type pre-commit --hook-type pre-push
uv run pytest -q
Most of the suite needs nothing but Python. tests/test_isolation.py builds a
throwaway Postgres database and drives the real app against it — it skips
cleanly if there is no server, so start one before trusting a green run on
anything touching accounts:
docker compose up -d postgres && uv run pytest -q
The pre-push hook runs the suite and refuses a push if anything skipped, for the same reason.
Supporting this
foilstack is free, AGPL, and costs you nothing to run. If it saves you an afternoon of typing card names into a spreadsheet:
The link is in the sidebar of every install, self-hosted ones included.
Licence
AGPL-3.0. The bundled JetBrains Mono subsets under
src/foilstack/web/static/fonts/ are licensed separately, under the SIL Open
Font License 1.1 — see OFL.txt beside them.
The network clause is the point: if you host this as a service for other people, those people are entitled to the source, including your changes.
Running it for yourself or inside your own business triggers nothing. A shop using this in the back office owes no one anything. The obligation begins when you offer it as a service to others.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file foilstack-0.2.5.tar.gz.
File metadata
- Download URL: foilstack-0.2.5.tar.gz
- Upload date:
- Size: 8.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d1c73778c5b85ff32866c5932bfc21c12cf2620bd15266e5206c0b20c22ad7bf
|
|
| MD5 |
f44a3c3fc2a36062c6d89c8b7800a79c
|
|
| BLAKE2b-256 |
30b8f1b40ea47c4292fa35898a3b89161a55dd50ddf4b0bf627f1e2c26506f8b
|
Provenance
The following attestation bundles were made for foilstack-0.2.5.tar.gz:
Publisher:
release.yml on foilstack/foilstack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
foilstack-0.2.5.tar.gz -
Subject digest:
d1c73778c5b85ff32866c5932bfc21c12cf2620bd15266e5206c0b20c22ad7bf - Sigstore transparency entry: 2582631878
- Sigstore integration time:
-
Permalink:
foilstack/foilstack@3a28811bf9acc06d615040ca7045baea2d8e614e -
Branch / Tag:
refs/tags/v0.2.5 - Owner: https://github.com/foilstack
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3a28811bf9acc06d615040ca7045baea2d8e614e -
Trigger Event:
push
-
Statement type:
File details
Details for the file foilstack-0.2.5-py3-none-any.whl.
File metadata
- Download URL: foilstack-0.2.5-py3-none-any.whl
- Upload date:
- Size: 8.5 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eec46231720f8ac7a08f82a7204f63f6057b2ed60a8b9bb832d3aed182dc9544
|
|
| MD5 |
9fbca24a7d40aa6857fd7e9bc857d5c4
|
|
| BLAKE2b-256 |
99e3c8cf775cebe3e1e3cf615c618d1da4b4683436bb415f8550efa0c529086a
|
Provenance
The following attestation bundles were made for foilstack-0.2.5-py3-none-any.whl:
Publisher:
release.yml on foilstack/foilstack
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
foilstack-0.2.5-py3-none-any.whl -
Subject digest:
eec46231720f8ac7a08f82a7204f63f6057b2ed60a8b9bb832d3aed182dc9544 - Sigstore transparency entry: 2582631881
- Sigstore integration time:
-
Permalink:
foilstack/foilstack@3a28811bf9acc06d615040ca7045baea2d8e614e -
Branch / Tag:
refs/tags/v0.2.5 - Owner: https://github.com/foilstack
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3a28811bf9acc06d615040ca7045baea2d8e614e -
Trigger Event:
push
-
Statement type: