Introduction
In this blog, we will create a Farcaster frame that allows users to mint NFTs on solana directly to their wallets or email after recasting and liking the cast. Try out the live demo here! Check out the video demo here.
What is a Farcaster frame?
A Frame empowers you to transform any cast into an interactive application. Seamlessly integrate features like polls, live feeds, galleries, NFT mints, etc. into Warpcast or any other Farcaster client. Frames extend the OpenGraph standard and turn static embeds into interactive experiences.
Tools
We're going to use various tools to build this:
- Next.js: We'll use next.js as our framework for creating the frame.
- Crossmint: To mint NFTs on solana to the user's wallet address or email, we'll use crossmint.
- Neynar: We will use neynar to check whether the user has recasted and liked the cast.
- Onchainkit: We will use the onchainkit to create the frame, and get the message from the frame on our server.
Setting up our NFT collection
Head over to Crossmint (staging for devnet), then head over to the Collections tab and click on "New Collection".
Then, fill in the metadata for your collection, and click on Next:
You can either select to create a new contract or import an existing one. I don't have a contract created so I'll create a new one:
Since we are going to be deploying on Solana choose Solana Devnet:
Here, you can enter any amount, since we won't be doing a claim but minting the NFTs to the user's wallet for free
Finally, confirm the details, and click on Create Collection:
Creating the frame
Creating a new next.js app
First, we need to create a new next.js app. We can do this by running the following command:
npx create-next-app farcaster-frame
This will create a new next.js app in the farcaster-frame
directory. So, navigate to the farcaster-frame
directory, and open it in your code editor.
Then, we need to install the packages we need:
npm install @coinbase/onchainkit @bonfida/spl-name-service @neynar/nodejs-sdk @solana/web3.js # npm
yarn add @coinbase/onchainkit @bonfida/spl-name-service @neynar/nodejs-sdk @solana/web3.js # yarn
Creating the frame
Firstly, let's clean the src/app/layout.tsx
file since we don't need any styling or metadata in the layout:
export const viewport = {
width: "device-width",
initialScale: 1.0,
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Then, head over to the page.tsx
file and replace the content with the following:
import { getFrameMetadata } from "@coinbase/onchainkit";
import type { Metadata } from "next";
const NEXT_PUBLIC_URL = process.env.NEXT_PUBLIC_URL;
const frameMetadata = getFrameMetadata({
buttons: [
{
label: "Mint an NFT",
action: "post",
},
],
image: {
src: `${NEXT_PUBLIC_URL}/default.png`,
aspectRatio: "1.91:1",
},
input: {
text: "enter...",
},
postUrl: `${NEXT_PUBLIC_URL}/api/frame`,
});
export const metadata: Metadata = {
title: "Mint an NFT",
description: "Mint an NFT",
openGraph: {
title: "Mint an NFT",
description: "Mint an NFT",
images: [`${NEXT_PUBLIC_URL}/default.png`],
},
other: {
...frameMetadata,
},
};
export default function Page() {
return (
<>
<h1>Mint an NFT</h1>
</>
);
}
Here, we are creating the frameMetadata
which contains the buttons, image, input, and postUrl. We just have a single button which creates an API request to the /api/frame
endpoint. And, we take in the wallet address, domain or email as input.
Then, we export the metadata which contains the frameMetadata
, and some other metadata like title, description, and openGraph.
Finally, we are returning a simple h1 tag with the text "Mint an NFT". This will be displayed on the website, but this has nothing to do with the frame.
As you can see we are using a default.png
image. This is the image which will be displayed in the frame at the beginning. You can replace this with your image. You can take a look at the image I am using here.
Creating the API endpoint
Now, we will create the API endpoint which will get called once the user hits the mint button. Create a new file called route.ts
in the app/api/frame
directory, and add the following content:
import {
FrameRequest,
getFrameMessage,
getFrameHtmlResponse,
} from "@coinbase/onchainkit";
import { NextRequest, NextResponse } from "next/server";
import { getDomainKeySync, NameRegistryState } from "@bonfida/spl-name-service";
import { Connection, PublicKey } from "@solana/web3.js";
export async function POST(req: NextRequest): Promise<Response> {
let input: string | undefined = "";
let recipientAddress = "";
const NEXT_PUBLIC_URL = process.env.NEXT_PUBLIC_URL;
let isEmail = false;
const body: FrameRequest = await req.json();
try {
const { message } = await getFrameMessage(body, {
neynarApiKey: "NEYNAR_ONCHAIN_KIT",
});
if (message?.input) {
input = message.input;
}
if (!input) {
return new NextResponse(
getFrameHtmlResponse({
image: {
src: `${NEXT_PUBLIC_URL}/error2.png`,
},
ogTitle: "Error",
})
);
}
if (input.slice(-4) === ".sol") {
const { pubkey } = getDomainKeySync(input);
const connection = new Connection("https://api.mainnet-beta.solana.com");
const { registry } = await NameRegistryState.retrieve(connection, pubkey);
const pubKey = new PublicKey(registry.owner);
recipientAddress = pubKey.toBase58();
} else if (input.includes("@")) {
isEmail = true;
recipientAddress = `email:${input}:solana`;
} else {
recipientAddress = `solana:${input}`;
}
return new NextResponse(
getFrameHtmlResponse({
image: {
src: `${NEXT_PUBLIC_URL}/success.png`,
},
})
);
} catch (error) {
return new NextResponse(
getFrameHtmlResponse({
image: {
src: `${NEXT_PUBLIC_URL}/error.png`,
},
ogTitle: "Error",
})
);
}
}
export const dynamic = "force-dynamic";
A lot is going on here, so let's break it down:
- We are getting the frame message from the request body using the onchainkit and then getting the input from the message.
- If there's no input, we return an error image. I have created a simple error image which you can use, or you can create your own or even use an image generator to create specific errors but for now, I will use the images that I have created.
- Then, we determine whether the input is a domain, email, or wallet address. If it's a domain, we get the wallet address from the domain using Bonfida. If it's an email, we set the recipient address to
email:${input}:solana
. So, the user can view it in his Crossmint wallet. If it's a wallet address, we set the recipient address tosolana:${input}
.
Now, let's mint the NFT to the user's wallet using crossmint. To do that add the following:
const env = process.env.CROSSMINT_ENV || "staging";
const crossmintURL = `https://${env}.crossmint.com/api/2022-06-09/collections/ENTER_YOUR_COLLECTION_ID/nfts`;
const crossmintOptions = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": process.env.CROSSMINT_API_KEY!,
},
body: JSON.stringify({
recipient: recipientAddress,
metadata: {
name: "Cool NFT ๐",
image: `${NEXT_PUBLIC_URL}/nft.png`,
description: "This is my cool NFT",
},
}),
};
Replace ENTER_YOUR_COLLECTION_ID
with your collection ID. You can get your collection ID from the collections tab in Crossmint. Also, replace the metadata and nft.png
with the image you want to use for the NFT. I have added the image to the public
directory in the next.js app.
We are using the CROSSMINT_API_KEY
env variable here, I'll go over where to get it in the getting to production section.
Then, replace return new NextResponse
with the following:
return new NextResponse(
getFrameHtmlResponse({
image: {
src: `${NEXT_PUBLIC_URL}/success.png`,
},
buttons: [
isEmail
? {
label: "View your NFT on crossmint",
action: "link",
target: `https://${env}.crossmint.com/user/collection`,
}
: {
label: "Your NFT will arrive soon",
},
],
})
);
Once the API call is successful, the user will see a success image, and a button to view the NFT on Crossmint. If the recipient address is an email, the user will see a button to view the NFT on Crossmint. If the recipient address is a wallet address, the user will see a message that the NFT will arrive soon.
Checking whether the user has recasted and liked the cast
This is an optional step but if we want to get some more engagement on the cast, we can make it compulsory for the users to recast and like the cast before they can mint the NFT. To do that, we can use neynar. Add the following to the route.ts
file:
if (process.env.WARPCAST_HASH && process.env.NEYNAR_API_KEY) {
const neynarURL = `https://api.neynar.com/v2/farcaster/cast?identifier=${process.env.WARPCAST_HASH}&type=hash`;
const neynarResponse = await fetch(neynarURL, {
headers: {
api_key: process.env.NEYNAR_API_KEY,
"content-type": "application/json",
},
method: "GET",
});
const data = await neynarResponse.json();
const reactions = await data.cast.reactions;
const hasRecasted = reactions.recasts.some(
(recast: { fid: Number }) => recast.fid === message?.interactor.fid
);
const hasLiked = reactions.likes.some(
(likes: { fid: Number }) => likes.fid === message?.interactor.fid
);
if (!hasRecasted || !hasLiked) {
return new NextResponse(
getFrameHtmlResponse({
image: {
src: `${NEXT_PUBLIC_URL}/error1.png`,
},
postUrl: `${NEXT_PUBLIC_URL}/api/frame`,
buttons: [
{
label: "Try again",
action: "post",
},
],
})
);
}
}
Here, we are making a call to Neynar's api and getting the cast. We are then checking whether the array of recasts and likes contains the user's fid. If it doesn't, we return an error image and a button to try again.
The final route.ts
file should look something like this:
import {
FrameRequest,
getFrameMessage,
getFrameHtmlResponse,
} from "@coinbase/onchainkit";
import { NextRequest, NextResponse } from "next/server";
import { getDomainKeySync, NameRegistryState } from "@bonfida/spl-name-service";
import { Connection, PublicKey } from "@solana/web3.js";
export async function POST(req: NextRequest): Promise<Response> {
let input: string | undefined = "";
let recipientAddress = "";
const NEXT_PUBLIC_URL = process.env.NEXT_PUBLIC_URL;
let isEmail = false;
const body: FrameRequest = await req.json();
try {
const { message } = await getFrameMessage(body, {
neynarApiKey: "NEYNAR_ONCHAIN_KIT",
});
if (message?.input) {
input = message.input;
}
if (!input) {
return new NextResponse(
getFrameHtmlResponse({
image: {
src: `${NEXT_PUBLIC_URL}/error2.png`,
},
ogTitle: "Error",
})
);
}
if (process.env.WARPCAST_HASH && process.env.NEYNAR_API_KEY) {
const neynarURL = `https://api.neynar.com/v2/farcaster/cast?identifier=${process.env.WARPCAST_HASH}&type=hash`;
const neynarResponse = await fetch(neynarURL, {
headers: {
api_key: process.env.NEYNAR_API_KEY,
"content-type": "application/json",
},
method: "GET",
});
const data = await neynarResponse.json();
const reactions = await data.cast.reactions;
const hasRecasted = reactions.recasts.some(
(recast: { fid: Number }) => recast.fid === message?.interactor.fid
);
const hasLiked = reactions.likes.some(
(likes: { fid: Number }) => likes.fid === message?.interactor.fid
);
if (!hasRecasted || !hasLiked) {
return new NextResponse(
getFrameHtmlResponse({
image: {
src: `${NEXT_PUBLIC_URL}/error1.png`,
},
postUrl: `${NEXT_PUBLIC_URL}/api/frame`,
buttons: [
{
label: "Try again",
action: "post",
},
],
})
);
}
}
if (input.slice(-4) === ".sol") {
const { pubkey } = getDomainKeySync(input);
const connection = new Connection("https://api.mainnet-beta.solana.com");
const { registry } = await NameRegistryState.retrieve(connection, pubkey);
const pubKey = new PublicKey(registry.owner);
recipientAddress = pubKey.toBase58();
} else if (input.includes("@")) {
isEmail = true;
recipientAddress = `email:${input}:solana`;
} else {
recipientAddress = `solana:${input}`;
}
const crossmintURL = `https://${env}.crossmint.com/api/2022-06-09/collections/56d9e10d-3caf-46a8-80a3-a507d034e51f/nfts`;
const crossmintOptions = {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
"x-api-key": process.env.CROSSMINT_API_KEY!,
},
body: JSON.stringify({
recipient: recipientAddress,
metadata: {
name: "Cool NFT ๐",
image: `${NEXT_PUBLIC_URL}/nft.png`,
description: "This is my cool NFT",
},
}),
};
const response = await fetch(crossmintURL, crossmintOptions);
await response.json();
return new NextResponse(
getFrameHtmlResponse({
image: {
src: `${NEXT_PUBLIC_URL}/success.png`,
},
buttons: [
isEmail
? {
label: "View your NFT on crossmint",
action: "link",
target: `https://${env}.crossmint.com/user/collection`,
}
: {
label: "Your NFT will arrive soon",
},
],
})
);
} catch (error) {
return new NextResponse(
getFrameHtmlResponse({
image: {
src: `${NEXT_PUBLIC_URL}/error.png`,
},
ogTitle: "Error",
})
);
}
}
export const dynamic = "force-dynamic";
That's it! We have created the frame. Now, let's get to production.
Getting to production
Create a new GitHub repository and push the code to the repository.
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/<username>/<repo_name>.git
git branch -M main
git push -u origin main
Then, head over to Vercel and import the repository. Once you have imported the repository, head over to the environment variables and add the following environment variables:
NEXT_PUBLIC_URL
: The URL of your next.js app.CROSSMINT_API_KEY
: The API key for crossmint. Head over to the API keys section in the Crossmint dashboard and create a new server key with permission to mint NFTs. Then, add the key as an environment variable.CROSSMINT_ENV
: If you switch over your app from testnet to mainnet, you can add the environment variableCROSSMINT_ENV
and set it towww
.
If you want to check whether the user has recasted and liked the cast, you can add the following environment variables:
WARPCAST_HASH
: The hash of the cast which you want to check has been recasted and liked.NEYNAR_API_KEY
The API key for neynar. You can get it from the neynar dashboard.
Then, head over to the domains tab and add your domain.
That's it! You have created a frame that allows users to mint NFTs on solana directly to their wallets or email after recasting and liking the cast.
Test the frame
You can go to the Frames validator on Warpcast and test your frame. If everything looks good, make a cast with the URL of your frame and share it with your friends to test it out!
Conclusion
This guide taught us how to create a Farcaster frame that allows users to mint NFTs on solana directly to their wallets or email after recasting and liking the cast. You learned a lot, now pat yourself on the back and share your amazing apps with me!