Certificates of originality on Ethereum and Polygon for an art studio
A platform where an art studio and its artists deploy an ERC-721 contract per collection or per certificate, without holding any crypto themselves, and drop the mint button, the certificate wall and the token gate into their own websites.
SUYE is an art studio and gallery in Amsterdam that works between physical and digital art. The artists it works with sell prints, canvases and clothing, and wanted each piece to come with something on a blockchain that says this is number so-and-so of so many. Some also sell NFT collections outright. I built the platform for that, alone, from June 2023 on, and it runs at certificates.suye.co. None of the people using it wanted to run a wallet, buy ETH for gas or read a block explorer, and the design follows from that.
One contract per collection, compiled when you press publish
Every collection and every certificate is its own ERC-721 contract. The Solidity source sits in the repository as a template with the name and symbol left as placeholders; when someone presses publish the server fills them in, compiles the file with solc 0.8.19 inside the request, and deploys the bytecode. So every contract on the chain carries the collection’s own name, and the exact source can be posted to the block explorer afterwards. Etherscan’s verification API wants one flattened file, so the OpenZeppelin imports are flattened ahead of time and kept next to the templates.
The collection contract holds a total supply, a mint price, a base URI, an address and a fee in basis points that takes a cut of every mint, a Merkle root and a switch for a whitelist, a per-address mint limit, and a number of tokens minted to a reserve address in the constructor. The owner can change all of those later without ever seeing a transaction: change the price in the dashboard and the server sends setMintPrice from the company’s wallet.
Nobody holds ETH
Each company on the platform has a managed wallet. The private key is encrypted with AES-256-GCM and stored in a Firestore collection whose security rule is a flat deny, so it is only ever read through the Admin SDK on the server.
The wallets are empty until they are needed. A main platform wallet per chain funds them just in time: if the company wallet cannot afford a gas estimate it is topped up to 0.01 ETH or 0.1 MATIC, then the deployment is estimated, the gas price is raised by ten per cent and the total by twenty, and the difference is transferred. On mainnet each deployment also costs one credit, which an admin assigns per company per chain; the testnets are free. The studio fronts the gas and settles it with the artist outside the system, since the alternative was teaching each artist to buy MATIC. When a collection has sold, the payout page reads every contract’s balance and withdraws it to an address from the company’s address book.
Publishing. Everything before the second checkpoint can be repeated safely; everything after it can be recovered from the transaction hash.
A certificate is a contract that mints itself out
In the data model a certificate is nearly the same document as a collection: a name, a symbol, a chain, a description, one image or a list of them. Both live under the company in Firestore and go through the same publish code. The difference is the contract. The certificate contract is a fraction of the size: its constructor takes the supply, the base URI and one address, and mints every token to that address before the deployment returns. No mint price, no whitelist, no withdraw; the owner can only change the base URI afterwards. For an edition of twenty prints the studio types the address the twenty tokens should go to, and sends them on as the prints are sold.
What makes it a certificate rather than a token is the wall. Each certificate carries named properties, such as the dimensions or the materials of the piece, and extra photographs, and these are written into a public JSON file per contract along with the ABI, the chain and the name. The certificate wall widget reads that file, shows the numbered tokens as a grid, and opens one as a page with the properties, the photographs and a link to that token on the block explorer. The documentation in the repository says certificates are non-transferable. The contract does not: it is a plain ERC-721 and the tokens move like any other.
Five widgets, one bucket
The customers’ websites are Squarespace shops and the like, so nothing on the platform expects to own a page. Instead there are five widgets, Svelte components compiled by Rollup into ES modules that a customer drops into their site with one script tag: a mint button, a collection viewer, a certificate viewer, a certificate wall and a token gate.
The widgets never call Firestore. A publish writes the contract’s ABI, chain, name, whitelist and display properties to a public Cloud Storage bucket, one JSON file per contract address, and the widgets read that. The mint button then talks to the chain directly through the visitor’s wallet, by way of Web3Modal configured for the one chain the collection is on. If the collection has a whitelist, the button rebuilds the whole Merkle tree in the browser from the address list in that JSON file and sends the proof with the mint, which means every whitelist on the platform is public. For a gallery’s list of collectors that was accepted.
The token gate is what the artists asked for most. A gate points at a collection, or at any contract on any of the four chains, and holds a list of voucher codes, one per line. The widget connects a wallet, asks it to sign the sentence Verify ownership of wallet, and posts the signature to the API. The server recovers the address, asks Alchemy which token ids of that contract it owns, and answers with the voucher on the matching line for each. On one artist’s Squarespace shop the voucher is the key to a members-only area, so a holder of token 12 gets code 12 and a script on the shop opens the door. A gate can also be a hosted page of its own, under a subdomain or the customer’s domain, with a theme editor and a sanitiser on any CSS the customer types in.
IPFS, until it was a folder
The token metadata was on IPFS from the start. Each publish pushed one JSON file per token through Infura’s IPFS API, wrapped them in a directory, and set the contract’s base URI to that directory’s CID on the ipfs.io gateway, so token 7 resolved to CID/7.
In October 2025 it stopped working, and a dedicated gateway did not fix it for long. Metadata now goes to a public folder in Cloud Storage, one object per token id, uploaded in batches of fifty with a half-second pause and three retries each, and new contracts point there. A migration script walks every published contract, fetches its old files from whichever gateway still answers, rehosts them, and can call setBaseTokenURI on the chain with the company’s key. It is dry-run by default and needs --apply plus a flag for each thing it may change.
The result is that the certificates are content-addressed on the chain and location-addressed off it. The token id and the ownership are immutable; the picture of the artwork is a file on Google’s servers that the studio controls. For a certificate of a physical print, whose proof is the print, I can defend that. For a collection sold as a digital original it is weaker than what buyers were promised in 2023.
A collection with no address
A publish is a compile, a metadata upload of a few hundred files and a wait for a block confirmation, and that does not fit in Vercel’s default function limit. The functions run for up to five minutes.
Five minutes is not always enough either, and for a long time a deployment that died after the transaction was broadcast left the studio looking at a collection with no contract address, although the contract was on the chain and the gas had been paid. The fix is the two checkpoints in the diagram: the ABI and source go to Firestore before the metadata upload, and the transaction hash goes there the moment the transaction is in the mempool. A finalize endpoint reads the receipt for that hash and completes the record, and the dashboard polls it every five seconds when a publish comes back without an address.
Polygon confirmations hung: ethers’ tx.wait() against Infura on Polygon would sometimes never return, so the wallet funding now polls for the receipt itself every three seconds with a two-minute limit.
The pieces and how they talk. The widgets on a customer’s site never touch Firestore: they read the bucket, the public API and the chain, and only the token gate posts back.
The platform serves four chains, Ethereum and Polygon and their test networks Sepolia and Amoy, and one token standard, ERC-721.