Back to blog

Making Private NFTs

Making Private NFTs

Steve

The acronym ‘NFT’ evokes a lot in the mind, whether you’ve been in the Web3 space for years or you’re just getting started. I’m sure most people think of animal pictures with a bunch of different traits, or perhaps a more modern rendition of small art pieces or memes. In some ways, owning an NFT gives you ownership of the content attached in the metadata, but what if you want that content to only be accessed by the owner? Much to the chagrin of the public, you can just right click and save most NFTs. This is where token gating comes into play, where an NFT acts as a key to the content that you purchased.

We’ve actually written about this before, particularly encrypting data on IPFS and unlocking with onchain credentials. However, there is a big gap between encryption and true privacy. With encrypted data on IPFS, you have to cross your fingers and hope that the encryption doesn’t break, but that’s against the nature of encryption, which is always changing and evolving. All it takes is a patient developer to break the encryption of a file that has been persisting publicly on IPFS for a few years. True privacy means you can’t access it unless you’re authorized, and NFTs prove to be a great way to authenticate.

CONCEALMINT

To prove this concept and show what’s possible, we built CONCEALMINT, a simple app that allows users to create and mint private file NFTs. After a user signs in with their wallet via Privy, they’ll view the CONCEALMINT NFTs they own. If they don’t any they can create a new one with a name, description, website link, cover image, and a file they want to token gate.

0:00
/0:14

Once created, it will show up in their feed where they can access the file. If the user sends the NFT to someone else or sells it, then the NFT will leave their feed and the new owner can sign in and see it in their feed. Owning the NFT will allow you access the private file that’s part of the NFT!

Currently the app lives just as an example and uses Base Sepolia, but you are more than welcome to view the source code here.

How It’s Made

Let’s cover some of the pieces being put together here to better understand how CONCEALMINT works under the hood.

Contracts and Metadata

Our NFT contract is a simple ERC721A with some small tweaks which you can check out here. The real piece that makes these NFTs different from others out there is the metadata. Here’s a quick view of one of the NFTs:

{
  name: 'Super Secrets',
  description: 'This NFT contains super secret stuff that only the owner can open 👀',
  external_url: '<https://stevedylan.dev>',
  image: 'ipfs://bafybeigkppl7e7vkfetauxr2pi3kfkjkvg6x5gghmo3qiofqqqpszkicme',
  file: 'bafybeie36dd3kv7ewtfyxakwgq7l5xyeoysukz7xqglw4bvzetarxf7efq'
}

The first four fields are standard on every NFT, however we have a custom file attribute that has a CID which can be used to fetch the file from the Files API. We’ll see that in a moment, but first we need a way to authorize who can access the file.

Auth

The app uses Privy as the primary auth provider, but to simplify things we’ve set the only available login method to be a wallet connection. The main benefits to this approach is we can use session token based auth to pull wallet information instead of dealing with some kind of signature based authentication from the wallet. When a user logs in with Privy we can have assurance that the wallet connected is the user’s and we can derive it from the session token. Below is some of the code used for the API endpoint to verify ownership of the NFT and accessing the file.

export async function GET(
	request: NextRequest,
	{ params }: { params: { id: string } },
) {
	const accessToken = request.headers.get("Authorization") as string;
	const auth = await privy.verifyAuthToken(accessToken.replace("Bearer ", ""));

	if (!auth.userId) {
		return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
	}

	try {
		const user = await privy.getUserById(auth.userId);

		const ownerData = await baseClient.readContract({
			address: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS as `0x`,
			abi: contract.abi,
			functionName: "ownerOf",
			args: [params.id],
		});

		const authorized = isAddressEqual(
			ownerData as `0x`,
			user.wallet?.address as `0x`,
		);
		
		if (!authorized) {
			console.log("Unauthorized");
			return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
		}

		// .... 

Token Gating

Since CONCEALMINT is minting NFTs, of course we want to use IPFS for things like the cover image and the metadata. This is info we want to be public and accessible to multiple parties. The Files API is what we used to upload our private file that was referenced in the NFT metadata, and with the proper authorization, we can access it. If we continue the API endpoint we saw in the previous section, then we’ll see this:

// Authorization, making sure the wallet of the session token matches the wallet of the owner of token ID

		const tokenURI = await baseClient.readContract({
			address: process.env.NEXT_PUBLIC_CONTRACT_ADDRESS as `0x`,
			abi: contract.abi,
			functionName: "tokenURI",
			args: [params.id],
		});

		const { data: nftData } = await ipfsClient.gateways.get(tokenURI as string);

		const nft = nftData as unknown as NFT;

		const url = await filesClient.gateways.createSignedURL({
			cid: nft.file,
			expires: 180,
		});

		return NextResponse.json({ url: url }, { status: 200 });
	} catch (e) {
		console.log(e);
		return NextResponse.json(
			{ error: "Internal Server Error" },
			{ status: 500 },
		);
	}
}

Once we know that the user is the owner of the NFT, we can make a quick call to the contract for that specific token ID URI, get the data using the IPFS SDK, then create a temporary URL for the user with the Files SDK. By using Pinata we can support both the public IPFS data for an NFT as well as the private data we only want accessible to the authorized party!

Wrapping Up

While this implementation is pretty simple, it’s the principles we really want to demonstrate here. There is a powerful yet undiscovered world of possibilities when you combine cryptographic ownership and identity with cloud storage infrastructure, and we can’t wait to see what you’ll build. Happy Pinning!

Subscribe to paid plan image

Share this post:

Stay up to date

Join our newsletter for the latest stories & product updates from the Pinata community.