Skip to content

Opening soon

Nothing can be bought, sold or minted here yet.

Wallets connect here when Station 4 opens for minting.

See Station 4

Minting soon

NFT metadata for Midnight

Draft 0.2 · · Not submitted to Midnight or anywhere else

Midnight has no standard for NFT metadata. One proposal covers the name, symbol and picture of any token (MIP-0018, on-chain token metadata, up for its vote on 2026-11-17); nothing covers traits, collections, reveals or how to fetch metadata for a shielded token without giving away which one you hold. This document is a complete, working answer to those, written so that it can be put forward as a Midnight Improvement Proposal.

It borrows what already works: the item file is a superset of the ERC-721 metadata JSON that every Ethereum marketplace reads, the media fields follow Cardano's CIP-25 and CIP-68, and the launch procedure is the provenance commitment the best Ethereum collections used. What is new is what Midnight needs and the others never had to solve: identity for native tokens, one on-chain commitment that covers everything, and private retrieval.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as in RFC 2119.

1. What it covers

  • An item file: one JSON file per NFT. Name, picture, traits.
  • A collection file: one JSON file per collection. It describes the collection and lists every item with the hash of its item file and of its picture.
  • One on-chain commitment: the SHA-256 of the collection file, held by the contract that mints the collection, with a flag that freezes it.
  • A sealed mint: how to commit to a collection before mint without revealing it, and how to reveal it afterwards so that anyone can check nobody chose who got what.
  • Private retrieval: how a wallet or a site reads metadata without telling a server which token it cares about.
  • How it sits beside MIP-0018, and what it would add to it.

It does not define how tokens are minted, transferred or sold, and it does not define royalties beyond stating them honestly (section 9).

2. Identity

An NFT on Midnight is, in the common case, a native token: a token type (a "colour") of which exactly one unit is ever minted. The colour is derived by the ledger from two things: the address of the minting contract and a 32-byte domain separator chosen by that contract.

colour = tokenType(domainSep, contractAddress)
  • An item is identified by its domainSep. The colour is never written in a metadata file: a reader derives it. (A file that states a colour could claim tokens that are not its own.)
  • kind says which pool the token lives in: "unshielded" or "shielded". It is fixed for the life of the token.
  • A collection's items all come from one contract.

A contract that keeps NFTs in its own state instead (the OpenZeppelin NonFungibleToken for Compact) is identified by contract address and token id; its tokenURI SHOULD point at the item file and everything else here applies unchanged. In that case midnight.kind is "contract" and domainSep is omitted.

3. The item file

One file per item, UTF-8 JSON, immutable once the collection is frozen.

FieldRequiredMeaning
schemaMUSTThe string "midnight-nft-item/1".
nameMUSTThe item's name as it is shown. At most 64 bytes of UTF-8; 32 or fewer is RECOMMENDED so it also fits fixed-width on-chain fields.
titleMAYThe name the item has of its own, apart from its number: "Mara Voss" in "Example #42 · Mara Voss". At most 48 bytes. If present, name MUST contain it. It belongs to the picture, not to the token, and is sealed with it (section 6).
imageMUSTURI of the picture (section 7). At most 128 bytes is RECOMMENDED.
image_sha256MUSTSHA-256 of the picture's bytes, 64 lowercase hex characters.
mediaTypeSHOULDMIME type of image, for example image/png.
descriptionMAYPlain text.
attributesMAYThe traits (below).
external_urlMAYA page about this item.
animation_urlMAYA moving or interactive version. If present, animation_sha256 MUST be present too.
filesMAYOther renditions: an array of { name, mediaType, src, sha256 }, for example the full-size original.
rarityMAY{ rank, of, score, tier, method } (section 8).
midnightSHOULD{ domainSep, kind }: the item's identity, so the file can be matched to a token without the collection file.
collectionMAY{ name, symbol }, for display when the item file is seen alone.

Unknown fields MUST be ignored by readers and MAY be added by writers.

Traits. attributes is an array of objects:

{ "trait_type": "Weapon", "value": "Relic blade" }
  • trait_type and value are REQUIRED. value is a string, a number or a boolean. A number MUST be a JSON number, not a string.
  • display_type MAY be "number", "boost_number", "boost_percentage" or "date", with the meaning the Ethereum marketplaces give them; max_value MAY accompany a number.
  • A trait_type MUST NOT appear twice in one item.
  • A trait an item does not have is left out. Do not write "None". The collection file says how many items lack each trait (section 4), which is what rarity tools need.
  • trait_type and string value are compared exactly. A writer MUST NOT emit two spellings of one value; the reference validator (tools/validate.mjs) flags near-duplicates, because two well-known Ethereum collections carry accidental one-of-ones made by a typing slip.

Example:

{
  "schema": "midnight-nft-item/1",
  "name": "Example #42 · Mara Voss",
  "title": "Mara Voss",
  "description": "Captain of the watch.",
  "image": "ipfs://bafybeiexampleexampleexampleexampleexampleexampleexample/42.png",
  "image_sha256": "0f3a…64 hex…",
  "mediaType": "image/png",
  "attributes": [
    { "trait_type": "Species", "value": "Android" },
    { "trait_type": "Rank", "value": "Captain" },
    { "trait_type": "Weapon", "value": "Relic blade" }
  ],
  "rarity": { "rank": 12, "of": 500, "score": 1.42, "tier": "Legendary", "method": "openrarity-ic" },
  "midnight": { "domainSep": "9c1d…64 hex…", "kind": "unshielded" }
}

4. The collection file

One file per collection, UTF-8 JSON. Its SHA-256 is what goes on chain (section 5).

FieldRequiredMeaning
schemaMUST"midnight-nft-collection/1".
nameMUSTCollection name, at most 64 bytes.
symbolMUSTShort ticker, 1 to 12 characters, as MIP-0018 limits it.
descriptionMAYPlain text.
image, image_sha256SHOULDThe collection's own picture.
banner_image, banner_sha256MAYA wide picture.
external_linkMAYThe collection's home.
supplyMUSTNumber of items. MUST equal the length of items.
stateMUST"sealed" or "revealed" (section 6).
revisionMUSTAn integer starting at 1, raised by one each time the file changes.
midnightMUST{ network, kind, decimals }. decimals MUST be 0. network is "mainnet", "preprod", "preview" or a chain's own name.
royaltyMAY{ bps, recipient, enforcement } (section 9).
licenceMAYA name or URI for the terms the pictures are under.
traitsSHOULD when revealedEvery trait type with every value and how many items have it: { "Weapon": { "Relic blade": 3, "Sidearm": 53 } }. Items without the trait are supply minus the sum.
bundleSHOULD{ uri, sha256, bytes }: one archive holding the collection file, every item file and every picture (section 7).
itemsMUSTOne entry per item (below), ordered by id.
provenanceMUST when sealed or revealed from a sealSection 6.

An entry of items when state is "revealed":

FieldRequiredMeaning
idMUSTThe item's number within the collection: an integer, unique.
domainSepMUST64 hex characters (section 2). Omitted only for kind: "contract".
nameMUSTSame as in the item file.
metadataMUSTURI of the item file.
metadata_sha256MUSTSHA-256 of the item file's bytes.
imageMUSTSame as in the item file.
image_sha256MUSTSame as in the item file.

When state is "sealed" an entry carries only id and domainSep; the commitments are in provenance (section 6).

A reader that has the collection file and trusts its hash can check every item file and every picture it later receives, from any source.

5. The on-chain commitment

The contract that mints a collection MUST hold, in public ledger state, for each collection:

  • meta_hash: the SHA-256 of the current collection file (32 bytes);
  • uri: where to get it;
  • meta_frozen: a flag that, once true, can never be false again and after which meta_hash can never change.

Only whoever controls the collection may change meta_hash and uri, and only while it is not frozen. Changing meta_hash is the update signal: a reader that sees a new hash fetches the new file.

A reader MUST treat a collection file as belonging to a contract only if that contract holds its hash. The file's own claims about where it belongs prove nothing.

A collection SHOULD be frozen as soon as it is revealed and checked. A marketplace SHOULD show whether a collection is frozen.

6. A sealed mint

Where the minter picks, or can predict, which item a mint produces, publishing the metadata before the mint lets the first minters take the rarest items. Publishing nothing lets the issuer decide afterwards who got what. A sealed mint prevents both.

Before mint. The issuer finalises every picture with its traits and whatever else belongs to it, shuffles them privately into slots 0 … supply-1, and publishes a collection file with state: "sealed". Its items list carries only the tokens' identities (id and domainSep). Its provenance.commitments list carries, for each slot, a commitment that reveals nothing:

traits_sha256 = SHA-256( canonical(attributes) )
rest_sha256   = SHA-256( canonical(rest) )
commitment    = SHA-256( salt || image_sha256 || traits_sha256 || rest_sha256 )
  • salt is 32 random bytes, different for each slot and kept secret. It matters: trait lists are easy to guess, and without it a commitment could be opened by trying them.
  • || joins raw bytes (32 + 32 + 32 + 32).
  • canonical(attributes) is the item's attributes array in the JSON Canonicalization Scheme (RFC 8785): object keys sorted, no spaces. An item with no traits uses [].
  • rest is an object holding whatever else in the item file belongs to the picture: title, description, animation_sha256, and files as the list of each rendition's sha256 in order. A member the item does not have is left out; an item with none of them uses {}.
  • So the commitment covers everything that belongs to the picture: the picture itself, the traits that make it rare, and the name and words that go with it. An item named in the open after the reveal could be given a name that was meant for another.
  • It does not cover what belongs to the token, which is only joined to a picture at the reveal: the identity, the number in name, external_url. Nor rarity, which follows from the traits of the whole collection.

The sealed file's provenance states how the reveal will be randomised, before anyone can know the outcome:

"provenance": {
  "method": "offset",
  "beacon": { "chain": "midnight", "network": "mainnet", "rule": "first-block-at-or-after", "time": "2026-12-01T18:00:00Z" },
  "unminted": "burned",
  "placeholder": { "image": "ipfs://…/unrevealed.png", "image_sha256": "…" },
  "commitments": ["…slot 0…", "…slot 1…", "…"]
}

The sealed file's hash goes on chain (section 5). Until the reveal, every token shows the placeholder.

After mint closes. The beacon is the hash of the first block whose timestamp is at or after the stated time, a block that did not exist when the sealed file was published. Then, with the tokens taken in order of id (the first is index 0):

offset        = (beacon block hash, as a big-endian integer) mod supply
slot of token = (index + offset) mod supply

So the issuer fixed which picture is in which slot without knowing which token id would land on it, and the minters chose token ids without knowing either.

The reveal. The issuer publishes a collection file with state: "revealed" whose provenance adds:

"sealed_sha256": "…hash of the sealed collection file…",
"beacon_block": { "height": 3460000, "hash": "…" },
"offset": 173,
"salts": ["…slot 0…", "…slot 1…", "…"]

and updates meta_hash on chain to the revealed file's hash. Anyone can then check, for every token, that SHA-256(salts[slot] || image_sha256 || traits_sha256 || rest_sha256) computed from its item file equals the commitment the sealed file held for its slot; that the sealed file's hash is the one the chain held before; that no identity changed; and that the offset follows from the named block. The reference validator (tools/validate.mjs) does exactly that, and its tests (tools/test.mjs) show each kind of tampering being caught: two tokens' portraits swapped, two tokens' names swapped, a trait, a title or a description changed after sealing, a different offset, a changed salt, a different sealed file, a changed identity.

The one thing a file cannot prove is that the named block really is the first at or after the stated time. A verifier checks that against the chain.

Tokens not minted when mint closes keep their slot. An issuer MUST say before mint what happens to them (never minted, or minted later in the open) in provenance.unminted: "burned" or "open".

An issuer that does not need any of this publishes a "revealed" file from the start and omits provenance.

7. Where files live, and reading them privately

Addresses. Every URI in these files is accompanied by the SHA-256 of what it points to, so any address works and any copy can be checked. For a frozen collection:

  • ipfs:// with a CIDv1 is RECOMMENDED for every URI; ar:// is equally acceptable.
  • https:// MAY be used, and readers MUST then verify the hash.
  • Files SHOULD be pinned with at least two independent providers.

Privacy. On Midnight the holder of a shielded token is private. A reader that asks a server for one item's file tells that server which token it is interested in, and the server sees its network address. Therefore:

  • A reader showing shielded tokens MUST NOT fetch item files or pictures one token at a time from a remote server. It fetches the collection file and the bundle, or the full set of files, and selects locally.
  • An issuer of a shielded collection MUST publish a bundle.
  • A reader SHOULD do the same for unshielded collections when it can afford to; for those, per-item fetching is permitted.
  • Nobody may run an endpoint of the form /metadata/{id} as the only way to read a collection.

The bundle is a plain tar archive (no compression needed for pictures) containing collection.json, items/<id>.json and images/<id>.<ext>.

8. Rarity

Optional. If present, rarity.method names how it was computed so that two tools can agree:

  • "openrarity-ic": OpenRarity's information content score. Each trait is worth minus the base-2 logarithm of the share of items with that value; a trait type an item lacks counts as a value of its own; the number of traits an item has counts as one more trait; the score is the sum divided by the collection's entropy; items holding a value no other item holds rank first.
  • "statistical": the sum over traits of one divided by the share of items with that value.

rank is 1 for the rarest; equal scores share a rank. tier is free text.

Rarity MUST NOT be written as a trait: it would change the thing it measures.

9. Royalties

Midnight's native tokens move from wallet to wallet without calling any contract, so today no contract can enforce a royalty on every transfer. A collection file states what is actually true:

"royalty": { "bps": 500, "recipient": "mn_addr1…", "enforcement": "market" }

enforcement is "market" (applied by the marketplace contract named by the commitment, on sales through it), "voluntary", or "none". A marketplace SHOULD honour a stated royalty. Nothing here should be read as a promise that it will be paid elsewhere.

10. Beside MIP-0018

MIP-0018 lets a contract announce, in an on-chain event, a token's name, symbol, decimals, image and image_sha256. It is how wallets and explorers will learn what to show without reading any file. A collection following this document SHOULD, once MIP-0018 is in force, emit one such event per item with:

MIP-0018 keyValue
namethe item's name
symbolthe collection's symbol
decimals0
image, image_sha256the item's
standardsincludes "midnight-nft/1"

This document proposes two further keys for a later MIP, so that a wallet holding only the event can find and verify everything else: metadata (the URI of the item file) and metadata_sha256.

During a sealed mint the event carries the placeholder picture; the issuer emits it again after the reveal, and the latest event wins.

11. How the fields line up

This documentERC-721 / OpenSeaCardano CIP-25 / CIP-68MIP-0018
namenamenamename
titlenonenonenone
descriptiondescriptiondescriptionnone
imageimageimageimage
image_sha256nonenoneimage_sha256
mediaTypenonemediaTypenone
attributesattributesfree-form propertiesnone
filesnonefilesnone
external_urlexternal_urlnonenone
animation_urlanimation_urlfilesnone
collection symbolcontract symbol()nonesymbol
collection filecontract-level metadata (ERC-7572)nonenone
on-chain meta_hashnone (BAYC's provenance hash is the nearest)nonenone
update signalMetadataUpdate event (ERC-4906)new mint transactiona new event

An item file is valid input to any tool that reads ERC-721 metadata: the extra fields are ignored.

12. Things an implementer must get right

  • Check hashes, always. A URI can serve anything. One famous collection's published provenance record was later found not to match 32 of its pictures.
  • Salt the commitments. Without salts a sealed mint is not sealed.
  • Seal the words with the picture. A name or a description published only at the reveal can be moved from one token to another.
  • Fix the beacon rule before mint and never afterwards. A rule that can be re-chosen is no rule.
  • One spelling per value. Generate trait strings from tables, never by hand.
  • Never serve shielded holders one file at a time.
  • Freeze. An unfrozen collection can still be changed by whoever controls it; say so until it is frozen.
  • Keep the contract's maintenance key if events are to be added after deployment: a contract deployed without a circuit for MIP-0018 can only gain one through its maintenance authority.

13. Open questions

  • Whether domainSep should be replaced or joined by a generic "token identity" object once MIP-0018 settles its own.
  • A registry: Midnight's token registry proposal lists NFT content as a non-goal. If a registry arrives, the collection file's hash and URI are what an entry should carry.
  • Compressed or deduplicated bundles for very large collections.
  • Whether the beacon should be allowed to be an external randomness source rather than a block hash.

14. Changes

  • 0.2, 2026-11-14. An item may carry a title: the name it has of its own, apart from its number. A sealed mint now seals everything that belongs to the picture (title, description, animation and other renditions) as well as the picture and its traits. Draft 0.1 sealed the picture and traits only.
  • 0.1, 2026-11-13. First draft.

Downloads

The tools are plain Node scripts with no dependencies. Keep the four in one folder.

  • JSON Schema (2020-12) for an item file.

  • JSON Schema (2020-12) for a collection file.

  • The checks themselves: hashes, canonical traits, commitments and the reveal. The other three tools import it.

  • Checks an item file, a collection file with its item files and pictures, and a reveal against its seal.

  • Makes a sealed collection file from a finished collection, and the revealed one from a beacon.

  • Builds a small collection, seals it, reveals it and checks that every kind of tampering is caught.