A Database With Good Lighting
How this gallery was built, for artists and for programmers: what happens to a work after it is collected, how the files get rescued, how the chain proves the history, and how a museum gets built out of one database file. With the art running through it.
On July 9, 2026 at 3:37 in the morning, there was an empty folder, an OpenSea API key saved in a text file, and one message to an AI: help plan out database collection and download of my NFT art vault. Everything on this site came out of that sentence. This is the story of how, written for two readers at once. If you make art, this is what happens to a work after someone collects it, and what you can do so it survives. If you write code, this is how a museum gets built out of a single SQLite file.
The banner above is fifteen works by fifteen artists, each file sent by the artist at full resolution, under the gallery's brand plate; they run through this story one by one. The full technical version, with the data model, the audit checks as code, the bug list, and every one of the fifteen at full size, is on GitHub: A Database With Good Lighting, on the portfolio, source in the along repo.
The plaque and the warehouse
Here is the part nobody warns you about, on either side of the transaction. An NFT does not store the art. The token is a permanent line on a blockchain. The picture it points to is a guest on IPFS, or Arweave, or an artist's web host, or a marketplace's cache, and any of those can go dark when the rent stops. When it does, the token is still yours. It just points at nothing. The artist's work is what actually disappears.
Think of a museum plaque with a photo of the painting printed on it. The plaque is bolted to the wall for good. The painting is in a rented warehouse across town. If the warehouse closes, the collector owns a very nice plaque. This whole gallery exists to keep the warehouse from mattering.

For the programmers: a token's tokenURI returns a URL to a JSON document; that document's image or animation_url field is another URL; the bytes at that second URL are the art. Two hops, both of them somebody else's server, and only the first hop is on-chain. This archive is a cache of the second hop with a checksum, plus a ledger of who owned the token when.
It was not a thought experiment. When the pipeline first pulled the original file for every piece I owned, twelve were already gone, the hosts dead. For nine of them a marketplace's cached thumbnail was the only copy left anywhere. I found that inside my own collection, the one I thought was safe. Today the vault holds 3,863 works by 850 artists across six chains, 4,840 original files and 67.5 gigabytes, 100.0 percent of them fetched from the exact source the artist declared. The live numbers are on the vault in numbers.
Eight weeks, sixty-three versions
Version one shipped in a day: the inventory, a twenty-five work pilot, the first gallery, live on a domain by hour fourteen. Then the conversation kept going. Two weeks of the wiki layer, the timeline, the world map, the stories, and the artists' recovered voices. A July weekend when the collection doubled, went to four chains, gained backups, and survived its first self-inflicted disaster. A hardening sprint: a full content security policy, structured data, an open dataset. A television page that plays the whole collection. Then the other half of the collection from a second blockchain, Tezos, and finally a fresh model reading the whole thing with no memory of having written it, which found stale numbers everywhere and one real bug that had been there since the first hour.
Every one of those versions passed the same gate before it went live: zero audit failures, zero appearances of a private wallet in the output. Nothing deploys unless a program says it can.

Who made this?
A token row has no artist column. That is the single most important fact about NFT data, and it explains half the wrong attributions you see on marketplaces. The token belongs to a contract; the contract maps to a collection; the collection has an owner address; the owner address is the artist. Except when it is not, which is often. Art Blocks and fx(hash) host hundreds of projects under a handful of shared contracts, so each token's own metadata has to name its project. Platform-owned contracts have the platform as the owner, so attribution falls to the minter, or to a trait, or to parsing a name out of a title. On Tezos the per-token creator is the only reliable signal, because per-artist contracts often have no owner at all. And above all of it sits a hand-authored curation layer, the one place taste is allowed to override the chain.
CREATE VIEW v_main AS
SELECT n.name AS title, n.contract_address, n.token_id, n.chain,
COALESCE(n.collection_slug, c.opensea_slug) AS collection, -- per-token beats per-contract
col.owner_address AS artist_key -- the artist, usually
FROM nfts n
LEFT JOIN contracts c ON c.address = n.contract_address
LEFT JOIN collections col ON col.slug = COALESCE(n.collection_slug, c.opensea_slug);
-- then: curation/overrides.json wins over every row above

For artists: if you want to be credited correctly by every archive that ever indexes your work, mint from a contract you deployed yourself, or make sure the token's own metadata carries your name. An archive can prove you made a contract from the chain, because the deployer address is a fact. It can only guess at a name in a title.
Getting the real files
The lazy way is to save whatever image the marketplace shows. That is wrong twice: it is a re-rendered copy, and it can differ from, or outlast, the artist's file. The honest way is to read the token's own metadata, follow the link the artist put there, fetch the bytes, and decide what kind of file it is from the bytes themselves, because the server's label lies more often than you would guess.
for nft in wallet_nfts(address):
meta = fetch(nft.token_uri) # the token's own record
url = meta.get("animation_url") or meta["image"] # the artist's declared source
data = fetch_with_gateway_rotation(url) # ipfs:// -> several gateways, politely
ext = sniff_type(data[:64]) # trust the bytes, not the header
save(f"{contract}/{token}.{ext}", data, sha256(data))
The very first bug of the project lived in that last line. The pilot saved a batch of videos that were really tiny image stubs: for static works the marketplace serves a preview that is an AVIF still inside a video-shaped container, and a naive sniffer calls it MP4. Fourteen fake videos were purged, and the rule that fell out of it became the whole philosophy. Trust only what the artist declared, verify the bytes, and label anything you had to swap in.

For artists, the five habits that do for free at the moment of minting what this whole archive does after the fact:
- Put the real file on IPFS or Arweave, not only on your website. Arweave is paid once and meant to last; IPFS lasts as long as someone pins it, so pin it in two places.
- Make the token's own metadata point at that file. The image field is the still; if the work moves, put the moving file in animation_url. Archives read those two fields and nothing else.
- Keep the master. The archive can only keep what was published. If you rendered at 8K and published at 1080p, the 1080p is the work forever.
- Mint from your own contract when you can, or through a platform that writes your address as the minter. That is what proves authorship later, not your name in a title.
- Say who you are somewhere permanent. Half the artists in this archive have a bio only because a marketplace profile happened to still be up when the pipeline ran. The ones who sent theirs directly are the ones that will not rot.

Reading the chain twice
A marketplace can relist a piece, hide it, or lose its history. The chain cannot. So every work's transfer history is read from two independent sources, the marketplace's event index and the chain explorer's own ledger, and merged. A mint is simply the first transfer, sent from the zero address. Purchases go one layer deeper: the transaction receipt and its logs name the marketplace contract that actually settled the sale, which is how a wall label here can say where a piece was minted and where it was collected and mean both.
Auctions are rebuilt bid by bid from raw calldata, working backward from each win. Editions are read from the contract, per token, so a polaroid that a marketplace summed to an edition of thousands now reads edition of twenty-five. And once a month a custody check reads the latest transfer of every work in the vault and confirms it still lands in a tracked wallet. For multi-edition tokens the test is net balance, because other collectors keep trading the same token id after our copy arrived; the naive version raised over a thousand false alarms before that was understood.

One bug here looked exactly like a fact. The chain-explorer client returned nothing for a reverted call, and also nothing for a rate-limited one, so for eight weeks a stage could record "this contract has no edition size" while it was simply being told to slow down. If you ask a busy clerk for a file and they say come back later, you have not learned that the file does not exist. The old code wrote down "file does not exist." The same thing happens to artists on marketplaces every day: a listing that says unknown creator is usually a summary that gave up, not the chain. The chain still knows.

The truth machine
A museum should be able to prove what it puts on the wall. So a program checks every build before it ships. Every internal link must resolve. Every mint venue must trace to a proven contract address. No price may appear anywhere, on any wall or in any story. No file under twenty kilobytes from a marketplace CDN may pose as an original. The private wallet must appear zero times. The number words in the essays must match the census, which is why the counts in this story are tokens the build fills in rather than numbers I typed. Even the em dash is banned, as a tell for machine prose. About a hundred of these run on every release, and a single failure blocks the deploy. After the deploy, they run again against the live pages, because green on a laptop and green on the open internet are two different facts.
For artists, what this means on your wall label: nothing there is typed in. The venue, the mint date, the edition size, the collecting date, and the count of your works in the vault all come from the chain or the database, and a program re-checks them before every release. If a label about your work is wrong, it is wrong in the data, and the fix is one row, not one page.

Bugs worth keeping
The gallery is polished. This story is not, on purpose, because the failures taught more than the features did. A cleanup script that knew one table's view of a folder that four tables share deleted 890 legitimate files: avatars, logos, hero videos. The audit failed the build on the spot, and the day-old backup restored every file in minutes. A query that asked the database for "the vault wallet" with no ordering got the wrong wallet, alphabetically. The marketplace's account API silently dropped delisted works and never re-added them; diffing the chain's authoritative list against the database found 41 works sitting in the vault the whole time. A shared Art Blocks deployer wallet "owned" every project it hosted, so one fake artist swallowed thirty-four works by other people until the collector, who knew the artist, noticed. And a stylesheet rule let hidden cards render visible while still classed hidden, so clicking one work opened another, a bug the collector narrowed down from scroll position alone.
The pattern in all of them: diagnose the class, not the instance. Every fix above ended as a reusable query or a new check, so the bug cannot come back, not just this one occurrence of it.

Two hands on the keyboard
One human directing. Three AI models across eight weeks. The AI did the engineering: a working archive and gallery by the end of the first day, three blockchains' APIs learned, auctions reconstructed from calldata, three hundred films transcoded for a television, a whole Spanish translation built and audited in a day, and thousands of pages rendered in about a hundred seconds. The habit that held it together was making the machine prove things. When something was in doubt the answer was never trust me; it was to write the check that settles it and read the check back.
None of the checks know what art is. The collector knew, from a thumbnail, that a video was a still. He knew which artist actually made a piece filed under a shared contract, which acquisition was a private deal the receipt forensics had mislabeled, which wallet must never appear, and which of two identically named accounts was the real person. He decided there would be no prices on the walls, that the artists' words belong next to the art, and that the whole thing should sound like a person and not a product. Every one of those decisions is now a curation file or a check, so the machine can hold the line he drew.


And what the AI got wrong, honestly: it deleted those 890 files; it stamped a generic category over hand-curated Art Blocks tiers on every run until the live site lost them; it wrote prose that sounded like a machine, which is why the em dash is now an audit check; it let the essays' numbers go stale for weeks; and it auto-linked the word photography inside an artist's own bio to a stranger's collection, which is why bios and artist statements no longer link out to other art at all.
Privacy, and what stays off the walls
The collection is public art, so the art is public, all of it, and the facts about it are published as a CC0 dataset. But the collector's quiet wallets stay quiet; the pipeline treats their activity as internal plumbing and a grep for the address must return zero hits before any deploy. No purchase or bid amount appears anywhere, because this is a museum and not a storefront. Only the built site ever leaves the machine; the database, the curation layer, and the keys never do. And the interactive works, hundreds of them, run in a locked sandbox, so an artist's program can play here without ever touching the gallery around it.

Limits, stated plainly
Some houses settle their bidding off-chain, so the site says won at auction more often than it can show bids, and says why. On Tezos the acquisition venues are labels from the indexer, not proven contract addresses. Some works survive only as marketplace cache copies; nothing can bring the artist's file back, and the label says so. A few edition sizes on layer-two chains stay honestly unknown. Some bios are still stubs and a few works are unattributed, in a declared section, because those are decisions waiting on a human, not defects. And the chain checks carry a date; the custody claim is true as of the date on it, not forever.

The fifteen
The banner at the top of this story is the gallery's front door frozen as one piece. Two of its fifteen cells hold works that are not tokens in this vault: Rebecca Rose's Rijksmuseum 13, her pick from a folder of eleven variants, and the file BoredJosei sent for the banner, a figure drawn entirely in coloured scribble and lit by the phone in its hand. Their rooms here hold the rest.


The full technical version of this story, with the data model, the audit checks as code, the bug list, and every one of the fifteen at full size, is on my portfolio: A Database With Good Lighting. The short version is how this was made. All fifteen artworks remain the property and copyright of their artists. They are here as part of the collection's record, not as anything for sale.