What the reveal pattern solves

An ERC721 token typically exposes metadata through tokenURI(tokenId). Before reveal, many projects want every token to point to the same placeholder JSON. After reveal, each token should resolve to its final metadata file.

A robust reveal pattern should:

  • return a placeholder URI until reveal is enabled
  • switch to final metadata without changing token ownership
  • avoid per-token storage for URI data when possible
  • prevent accidental or unauthorized reveal
  • remain compatible with marketplaces and wallets

A common mistake is storing a full URI for every token. That works, but it increases gas costs and makes reveal logic harder to manage. A better approach is to use a base URI plus a token ID suffix after reveal.


Design goals

For this example, the contract will support:

  • ERC721 minting
  • a placeholder metadata URI before reveal
  • a base URI after reveal
  • an owner-only reveal action
  • optional one-way reveal, so metadata cannot be hidden again

This pattern is especially useful for:

  • NFT collections with delayed art release
  • game items whose final stats are assigned later
  • membership passes that initially show generic content
  • launch phases where metadata should not be visible too early

Core implementation

Below is a compact but production-oriented contract using OpenZeppelin libraries.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract RevealedCollectible is ERC721, Ownable {
    uint256 private _nextTokenId = 1;

    // Placeholder shown before reveal
    string private _placeholderURI;

    // Base URI used after reveal
    string private _baseTokenURI;

    // Reveal flag
    bool public revealed;

    constructor(
        string memory name_,
        string memory symbol_,
        string memory placeholderURI_,
        string memory baseTokenURI_
    ) ERC721(name_, symbol_) Ownable(msg.sender) {
        _placeholderURI = placeholderURI_;
        _baseTokenURI = baseTokenURI_;
    }

    function mint(address to) external onlyOwner returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        return tokenId;
    }

    function reveal() external onlyOwner {
        revealed = true;
    }

    function setPlaceholderURI(string calldata newPlaceholderURI) external onlyOwner {
        require(!revealed, "Already revealed");
        _placeholderURI = newPlaceholderURI;
    }

    function setBaseURI(string calldata newBaseURI) external onlyOwner {
        require(!revealed, "Already revealed");
        _baseTokenURI = newBaseURI;
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        _requireOwned(tokenId);

        if (!revealed) {
            return _placeholderURI;
        }

        return string.concat(_baseTokenURI, _toString(tokenId), ".json");
    }
}

This contract keeps the reveal logic straightforward: before reveal, every token returns the same placeholder URI; after reveal, each token uses the base URI plus its token ID.


How the pattern works

Before reveal

When revealed == false, tokenURI() returns _placeholderURI for every minted token. This URI usually points to a JSON file such as:

{
  "name": "Mystery Collectible",
  "description": "Metadata will be revealed soon.",
  "image": "ipfs://.../placeholder.png"
}

This is useful because marketplaces can still display a valid metadata object, even though the final art is hidden.

After reveal

Once the owner calls reveal(), the contract starts returning:

baseURI + tokenId + ".json"

For example:

  • ipfs://QmBase/1.json
  • ipfs://QmBase/2.json
  • ipfs://QmBase/3.json

This approach is efficient because the contract does not need to store a URI for each token.


Why this approach is safer than per-token URI storage

A per-token mapping such as mapping(uint256 => string) is flexible, but it has drawbacks:

ApproachProsCons
Per-token URI mappingMaximum flexibilityHigher storage cost, more complexity
Base URI + token IDSimple, gas-efficient, easy to auditRequires consistent off-chain file naming
Single placeholder then base URIGood for reveal workflowsNeeds a clear reveal policy

For most collections, base URI plus token ID is the best balance of simplicity and cost.


Best practices for reveal control

1. Make reveal one-way if possible

If metadata is already public, allowing the owner to hide it again can confuse users and marketplaces. A one-way reveal is easier to reason about and reduces trust assumptions.

In the example above, revealed can only move from false to true.

2. Lock URI changes after reveal

If the base URI can be changed after reveal, the owner could redirect metadata to a different server or IPFS folder. That may be acceptable in some projects, but it should be intentional.

A stricter version would freeze the base URI once revealed:

function reveal() external onlyOwner {
    revealed = true;
}

And remove any post-reveal URI setters. If you need flexibility, document it clearly.

3. Use immutable or well-controlled ownership

The reveal switch is a privileged action. If the owner key is compromised, metadata can be manipulated. Consider:

  • a multisig owner
  • a timelocked admin process
  • a dedicated deployment wallet with limited exposure

4. Validate token existence in tokenURI()

Always check that the token exists before returning metadata. In OpenZeppelin ERC721, _requireOwned(tokenId) ensures the token was minted and has not been burned.

5. Keep off-chain metadata deterministic

If you use tokenId.json, make sure your storage layout is stable and predictable. A common convention is:

metadata/
  1.json
  2.json
  3.json

This makes it easy to generate files and verify them before deployment.


Handling placeholder metadata correctly

A placeholder should still be a valid ERC721 metadata object. Marketplaces expect fields like name, description, and image. If the placeholder JSON is malformed, some platforms may fail to display the token properly.

A good placeholder JSON might look like this:

{
  "name": "Revealed Collectible #1",
  "description": "The final metadata has not been revealed yet.",
  "image": "ipfs://QmPlaceholder/hidden.png",
  "attributes": [
    {
      "trait_type": "Status",
      "value": "Hidden"
    }
  ]
}

This keeps the token visible while clearly signaling that final metadata is pending.


Common implementation mistakes

Returning an empty string before reveal

An empty tokenURI() result is not a good placeholder. Some clients treat it as missing metadata, which can lead to inconsistent display behavior.

Using tokenId without existence checks

If tokenURI() does not verify ownership or mint status, callers may query nonexistent tokens and receive misleading results.

Forgetting to freeze metadata policy

If users expect a permanent reveal, but the contract allows the owner to change the base URI later, trust can be damaged. Decide early whether metadata is mutable or immutable.

Hardcoding file paths incorrectly

If your contract returns baseURI + tokenId, your off-chain files must match the same format exactly. For example, if the contract appends .json, your storage must use that extension too.


Extending the pattern

You can adapt this design in several useful ways.

Reveal by timestamp

Instead of manual reveal, you can reveal automatically after a specific block timestamp:

uint256 public revealTime;

function isRevealed() public view returns (bool) {
    return block.timestamp >= revealTime;
}

This is useful when the reveal date is announced in advance. However, manual reveal is often simpler and more flexible.

Reveal in phases

For larger collections, you may want multiple stages:

  • phase 1: placeholder
  • phase 2: partial reveal
  • phase 3: final reveal

That can be implemented with a revealStage variable and multiple base URIs. Keep the logic explicit so users know which stage is active.

Use encrypted metadata off-chain

Some projects store encrypted metadata and publish the decryption key at reveal time. This is more complex and usually unnecessary unless you need stronger secrecy before launch.


Testing the reveal behavior

A reveal contract should be tested for both pre- and post-reveal behavior. At minimum, cover these cases:

  • tokenURI() reverts for nonexistent tokens
  • tokenURI() returns placeholder before reveal
  • tokenURI() returns final URI after reveal
  • only the owner can call reveal()
  • URI setters are blocked after reveal, if you enforce freezing

A simple test sequence might look like this:

  1. Deploy contract with placeholder and base URI.
  2. Mint token 1.
  3. Confirm tokenURI(1) returns placeholder.
  4. Call reveal().
  5. Confirm tokenURI(1) returns baseURI + "1.json".

This verifies the core behavior without relying on marketplace-specific assumptions.


Deployment and operational checklist

Before deploying, confirm the following:

  • placeholder metadata is valid JSON
  • final metadata files are uploaded and accessible
  • token IDs match the off-chain file names
  • the owner account is secure
  • reveal policy is documented for users
  • if using IPFS, the content is pinned and immutable

If you are using a centralized server instead of IPFS, make sure you understand the trust trade-offs. A centralized endpoint can be changed or taken offline, which may break metadata resolution.


When to use this pattern

Use a reveal pattern when:

  • you want to hide final artwork until a specific moment
  • you need a simple, auditable metadata switch
  • your collection uses predictable file naming
  • you want to avoid storing per-token metadata on-chain

Avoid it when:

  • metadata must be permanently known at mint time
  • each token needs unique on-chain generated metadata
  • the project requires fully immutable metadata from the start

In those cases, a different architecture may be more appropriate.


Conclusion

A safe ERC721 metadata reveal pattern is mostly about clarity: clear placeholder behavior, clear reveal rules, and clear post-reveal URI resolution. By keeping the contract simple and the metadata layout deterministic, you reduce gas costs and make the collection easier to audit and operate.

The example in this tutorial provides a solid foundation for most NFT reveal workflows. From here, you can add phased reveals, timelocks, or stricter immutability depending on your project’s trust model.

Learn more with useful resources