One name each
Eight cards came out of the catalog named after themselves twice — Blood Crypt // Blood Crypt — and ManaBox refused every row carrying one. Scryfall shipped a reversible Blood Crypt in the same expansion, on the same day, as the ordinary one, so the two tied on every count the printing order knew about and the order of the file decided which named the card. A card is no longer named after the printing folded back on itself.
Easy to leave
A collection comes back out as a file. ManaBox's own shape, held to their exports row for row — 403 of 403 match outside the id they issue and a currency they print beside a blank price — or a shape of ours carrying a column per field a record holds. A format is a table of field against column, so an export is that table read backwards, and the next tracker is a binding rather than another writer.
Detection explained
A detection carries every measurement it weighed now, and the first check that turned it down, and a tally runs that over a directory of frames at any flood step. Asked what drops a card tilted away from the camera, the answer is the hull test — and a lean is not what it refuses. Drawn at a known angle a card clears every check out to 60 degrees, while the photographs that fail have a silhouette gone soft, which grows the hull faster than the four corners fitted inside it, so face on in poor light reaches it as readily as tilted does.
No longer a file somebody remembers
The server fills its own artwork store now, a capped run each refresh, so a scan stops going stale a set at a time. A store copied in is a shortcut past the first build rather than a thing someone has to remember. Scanned cards carry a condition, graded for a box and corrected per stack.
A scan you can keep
A scan lands in a scratch list the tab outlives, and the list is reviewed before any of it becomes a collection entry: a stack per card with its own count, the printing and the finish pickable, a catalog search for a card the reader never matched, and a commit that drains through the same import the CSV path uses. An uncertain read waits to be agreed with rather than arriving in the list on its own — keeping one is also how a wrong read reaches the list to be put right, so it cannot stand as naming the card.
A camera that reads a card
The scanner is in, end to end: perceptual hashes of every artwork Scryfall serves, built into a store and published beside the catalog, and a scan tab that is the camera itself — a frame read at 1280, a card found in it and straightened, and the printing it names with how far off the runner-up was. Where that margin is too thin the runners-up are offered instead of a guess being taken as an answer. The reading engine is Rust compiled to wasm and reached over a C ABI, so the same calls will serve a native app; it is committed rather than built by whatever deploys the app, and held byte for byte to what its source builds as well as to what a native build answers.
What four reviews found
Fifteen commits over the tree, and the ones that would have bitten are all in paths nothing exercises until they fail: a bucket with no manifest could never receive its first catalog, because the sweep that makes room for an upload opened by reading the one it was supposed to produce. An import ended itself on any dropped connection, a TypeError from fetch having been read as a verdict from the PDS. A part re-sent after its answer was lost wrote those cards a second time, and a discarded import came back on the next app open.
A migration makes the cache stale
An upgraded container read its cache as current, skipped the sync and exported a column that migration had just added, publishing nulls over a correct artifact. bulk_sync now records the schema it synced under, so a cache from anything earlier reads as unsynced: the refresh downloads again and the export refuses. -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgD8oXmYAt6UaSn3AemwmXd+yv1d zVfGqhV6IJaTUsPScAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQMy/+Z+13H6yCcppueBIYSyJySDCLjElcnHHAlxXr2vxAEdUVgl98Xn6eb9Tn7HX/A DSJlFLkug3BAr3jX/ilgE= -----END SSH SIGNATURE-----
Both sides of the card
The cards file carries a two-sided card's faces and its keywords, and the prints file names which artwork each printing shows — the three parts the base pair was costed for, landing together because a format change costs a re-upload of everything. A transform card could answer nothing about its colors, cost or power; the queries where our search disagrees with Scryfall's went from eleven missing results to one. The client reads past a column it has no reader for, so an artifact may be published before the client that reads it. -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgD8oXmYAt6UaSn3AemwmXd+yv1d zVfGqhV6IJaTUsPScAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQItXWwZ0rgfgrMe4qkuOGMlN1ZdFvyaZQrCqP/NB7Q1XdqtHeQZVa/V8cFPhBVMrD7 4dMwhRzYhNM4ZfsPfY0QU= -----END SSH SIGNATURE-----
The artwork, and a faster export
A printing now carries which artwork it shows, the thing an art match narrows to and the first the scanner will want from the cache; nothing reads it yet, and it fills on the next sync. The export stopped sorting a hundred thousand joined rows to find the order it writes in and reads an order written at sync instead: 1.64s to 0.80s, for the same bytes. -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgD8oXmYAt6UaSn3AemwmXd+yv1d zVfGqhV6IJaTUsPScAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQJepGPTEXgA/W3q40ZHAdMoxv7PtcXzCGbEksjwoh1Cc4q2qSznMr2RZKus9iTVFtW UykWtAq0l1ge+75cx8LQA= -----END SSH SIGNATURE-----
The client reads past what it does not know
A file may carry columns this client has no reader for, which is the only way one is ever added: a client already loaded fetches on its own schedule, so refusing a field list that is not exactly the one it reads takes the catalog off every browser already out there. A column that moved is still refused. -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgD8oXmYAt6UaSn3AemwmXd+yv1d zVfGqhV6IJaTUsPScAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQIFxqGYRbYYJY4QdKEjtznPujXkvhGJMhX1x2sd6es/GIXMAucKHTJuNgQ5VNSFEdC n+pC8Mu8ELNkh+0ah2ZQE= -----END SSH SIGNATURE-----
The avatar renders
A profile picture is read through the session and drawn from an object URL, which the policy refused, so it has never once appeared. The dev server names the catalog's own origin for the same reason. -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgD8oXmYAt6UaSn3AemwmXd+yv1d zVfGqhV6IJaTUsPScAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQLqFfrhpLCl3Nm5U23FG/26l2PxfXPdTsbt+Sp3xQsJuBZdQEbRHOSzLVk3dbXDJz+ rvqCTkbnL0FLQcDZBmEA8= -----END SSH SIGNATURE-----
An app, not a page
The shell holds the viewport and nothing outside it scrolls, and every icon the app ships is composed from the one drawing — inset where a launcher masks it, a silhouette where it throws the color away. -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgD8oXmYAt6UaSn3AemwmXd+yv1d zVfGqhV6IJaTUsPScAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQGRiNvvsWOXGz1QP7/yT/5dyqXH5rz9js6j3reZ55ngFi67SJHG1Uj1bxSqXsjkyT6 myzOUidRQb/rk+AnVQuwc= -----END SSH SIGNATURE-----
v0.7.0 -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgD8oXmYAt6UaSn3AemwmXd+yv1d zVfGqhV6IJaTUsPScAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQJgBDjKMd21f+3ijU30NdG8+eHbhkX37lKAS0Ii/t5qytN8Ue8LX30maezInvyTUuN 4k9OFuDcniy7zUsVkIDw8= -----END SSH SIGNATURE-----
v0.6.0 -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgD8oXmYAt6UaSn3AemwmXd+yv1d zVfGqhV6IJaTUsPScAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQBZ6nxVGte5LdSUeIXr26kyL06VzkZ54oSsKNeHVVr7/tJpNJlhiWkMzKMpOJoXaE/ PpDC9W7iVCKMAw5PqM3Qg= -----END SSH SIGNATURE-----
Deployed
The app is on Pages and the catalog in R2 behind its own domain, each artifact set under a prefix of its own so a scanner index can land beside the catalog without either claiming the name a client reads first. The binary uploads after a refresh rather than handing the directory to a shell command, which is what a host running a container with no checkout can actually do, and it holds the credentials itself. A deployed catalog can be held to the headers a client needs without a checkout to compare against.
A collection, imported
Reads a ManaBox export into stacks, plans them against what the account holds and puts the whole file in the repo as import parts in seconds — three calls for 4,333 stacks — then drains them into card records over hours at whatever the PDS will take. The repo holds the work rather than the device, so closing the tab, wiping the browser or signing in elsewhere all resume. Cards still waiting in a part count as owned wherever they are shown, and copies can be taken off one before it lands. The server picks record keys, the collection paints from its last read before the repo answers, and a tab remembers where you left it.
The collection, editable
Signs in over OAuth on granular scopes, reads the account's own records and writes them back: add a printing or another copy, take one off, regrade it, move it, tag it, note it, mark it a proxy. Containers are an opt-in layer with a chosen destination, and deleting one returns its cards rather than orphaning them. Four routes behind a bar that stands up as a rail on a laptop, a printings gallery you pick by art, and set completion counted from what you hold. Record types are generated from the lexicons.
The catalog, browsable
Reads the 3.99MB catalog into IndexedDB and searches 37,564 cards in the browser, ranked on EDHREC popularity where there is one and reprint count where there isn't. 988 sets to browse, printings filtered by language, and Wizards' own mana symbols with the notices that obliges. The artifact gained stats, flags, artist and rank; its print ordering became deterministic.
The card cache, and the catalog a client needs
Streams Scryfall's Default Cards into an 81MB SQLite cache, exports the 3.67MB catalog from it, and serves that for local development. Lexicons enumerated and validated against atproto's own implementation. No client yet — build order step 4 of 6. -----BEGIN SSH SIGNATURE----- U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgD8oXmYAt6UaSn3AemwmXd+yv1d zVfGqhV6IJaTUsPScAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 AAAAQI/8M6NpuRVDRI/D54Pisd1UZ8IiOITSD9yt9+MbOTJx9LfDLpkZKqVA8d58oxbvfY ZbovEk+quc05LP5dmH/wM= -----END SSH SIGNATURE-----