In recent weeks, Farcaster Frames have gained a lot of attention for their endless potential. They can turn a regular social post into a dynamic experience that any developer can create, letting you collect reactions, play mini-games, or even showcase and mint NFTs directly from the feed. The latter is the most common use case by far. In this tutorial, you'll learn how to implement this kind of frame using the Phosphor APIs.
Phosphor APIs are an all-in-one solution that helps you create, manage, and sell NFTs seamlessly. Phosphor API is for any type of developer to facilitate the convenience of paying blockchain fees with a credit card. This simplifies the process for users unfamiliar with cryptocurrency and/or private keys, while also enabling full ownership of smart contracts for those that want to retain total control over the collections deployed on-chain.
We'll leverage the API to allow Farcaster users to claim an NFT for free (this means, the transaction gas is paid by the NFT creator).
Our goal
We’re going to create a frame that:
Gets the item data from a listing identifier.
Shows the item image and a button users will press to mint. The button shows the call to action with item title and quantities (remaining / total supply).
Requires that the user has liked the cast, and if not, asks the user to like and try again.
After mint, shows a success or error message.
To make things really simple, we'll use the raw item image, without any overlay, and a single button, which at the same time will provide feedback messages.
You can access the finished project repository at GitHub and a working version here at https://phosphor-frame-tutorial.vercel.app
Before we start
We'll assume that you already have created a listing, and have the corresponding listing identifier. If you haven’t, in the repo, you can find a this script to help you create your own collection and get a listing identifier. Change the parameters in the script to your preferences and then run it using:
node create-free-mint-listing.mjs
To create a listing, you’ll need a Phosphor API key. Get your API key from https://www.phosphor.xyz/developer.
Recommended reading: The Developer Guide to Getting Started with Phosphor
Getting started
The concept behind Frames is actually pretty simple. Essentially, It’s an extension of the Open Graph protocol, relying on <meta>
tags and optionally a backend handler for buttons (refer to the specification). However, manually crafting <meta>
tags can quickly become tedious and state management can become overwhelming.
That’s why we’ll use the excellent frames.js library, which abstracts a lot of the frame state and generation complexities by leveraging familiar React patterns. Additionally, we'll base our repository on the frames.js starter template – a Next.js project that provides a tool to debug your frames locally (eg. user impersonation, button testing, mock cast reactions, and more).
Exploring the code
Follow the instructions on the provided README to install dependencies, start the server, and familiarize yourself with the project structure.
A frame created with frames.js and React consists of two parts:
A React component (in our case,
app/listing/[slug]/page.tsx
) which contains most of our frame code.A route handler for POST requests (in our case,
app/listing/[slug]/frames/route.ts
, which is completely handled by a frames.js function).
Implementation steps
All of the following code snippets can be found in app/listing/[slug]/page.tsx within the phosphor-frame-tutorial repository.
1. Get the frame message and validate it
The first step involves retrieving the frame state (via search parameters) and the signed message generated when a user presses a frame button. For this, we use the frames.js function getFrameMessage.
export default async function Home({ params, searchParams }: NextServerPageProps) {
const previousFrame = getPreviousFrame<State>(searchParams)
let listingId = params.slug
const [state] = useFramesReducer<State>(reducer, initialState, previousFrame)
// Keep in mind that frameMessage is undefined on the first frame
const frameMessage = await getFrameMessage(previousFrame.postBody, {
...HUB_OPTIONS,
})
console.log({ frameMessage })
// Check the frame payload for validity
if (frameMessage && !frameMessage.isValid) {
throw new Error("Invalid frame payload")
}
To validate the frame message and determine whether the user has liked the cast, we need access to a Farcaster Hub. There are many alternatives available, but for this tutorial we’ll use the free Pinata Farcaster Hub. You can switch to a different hub (such as Neynar) by setting a different value for FALLBACK_HUB_OPTIONS
in the app/constants.ts
file.
We use the slug
parameter to pass the listing identifier. Why call it slug
in the code? Simply to reuse the type NextServerPageProps
(from frames.js/next/server
), which already defines it.
2. Get the recipient’s address
Next step is to get the recipient's address from the frame message:
const userAddress: string | undefined =
frameMessage?.requesterVerifiedAddresses?.[0] ||
frameMessage?.requesterCustodyAddress
We adopt a straightforward approach by using the first verified address, if available, and defaulting to the custody address otherwise. While we can count on the custody address being always available for a valid message, the verified address is more user-friendly. It’s worth noting that during the first frame, when frameMessage
is undefined, userAddress
will also be undefined.
3. Get the listing data and validate it
Next, we get the listing data and validate it. To achieve this, we use the GET /listings/{listing_id} endpoint.
const listing = await requestPhosphorApi(`/listings/${listingId}`)
let errorMessage = await validateListing(listing, userAddress)
The validation process includes a call to the GET /listings/redemption-eligibility endpoint, which will provide us with additional information about the user’s eligibility at that moment.
Learn more about listings and listing policies at the Phosphor documentation site.
async function validateListing(listing: any, address: any) {
if (!listing || listing.error) {
return "Listing not found"
}
if (!listing.payment_providers.includes("ORGANIZATION")) {
return "Invalid listing"
}
if (listing.quantity_remaining === 0) {
return "No more items remaining"
}
if (listing.end_time && new Date(listing.end_time) < new Date()) {
return "Listing has ended"
}
if (address) {
const eligibility = await requestPhosphorApi(
`/listings/redemption-eligibility?listing_id=${listing.id}ð_address=${address}`
)
if (!eligibility?.is_eligible) {
if (eligibility?.quantity_claimed === eligibility?.quantity_allowed) {
return "You have already minted this item"
}
return "You are not eligible to mint this item"
}
}
}
4. Add other checks
Once the listing validation is completed, we add a validation about the cast itself. In this case, we added the requirement of liking the cast.
if (!errorMessage && frameMessage?.likedCast === false) {
errorMessage = "Mmmh... maybe if you like it first? Try again"
}
5. Create a purchase intent
Now, with an eligible user address at hand and all validations passed, we are ready to create a purchase intent using the POST /purchase-intents endpoint. The response can be used to check errors or to monitor the status of your request (see ideas for improvements below).
Learn more about purchase intents at the Phosphor documentation site.
if (!errorMessage && userAddress) {
const purchaseIntent = await requestPhosphorApi("/purchase-intents", {
method: "POST",
body: JSON.stringify({
provider: "ORGANIZATION",
listing_id: listing.id,
quantity: 1,
buyer: { eth_address: userAddress },
}),
})
if (purchaseIntent.error) {
console.error({ purchaseIntent })
errorMessage = "There was an error minting this item"
} else {
console.log({ purchaseIntent })
errorMessage =
"Your item has been minted successfully. It could take up a few minutes to arrive..."
}
}
6. Get the item data
Regardless of the status of our frame flow, we’ll present the item image. To get its URL, we use both the GET /items/{item_id} and the GET /collections/{collection_id} endpoints.
Learn more about items and collections at the Phosphor documentation site.
const { imageUrl, title, collectionName } = await getItemData(listing)
async function getItemData(listing: any) {
if (!listing || listing.error) {
return {}
}
const item = await requestPhosphorApi(`/items/${listing.item_id}`)
const collection = await requestPhosphorApi(
`/collections/${item.collection_id}`
)
let imageUrl = item.media.image.full
if (!imageUrl) {
imageUrl = collection.media.thumbnail_image_url
}
const title = item.attributes.title
const collectionName = collection.name
return { imageUrl, title, collectionName }
}
7. Render the frame
Finally, we render our frame. Frames.js handles the generation of all necessary <meta>
tags based on the <Frame*>
elements.
To make debugging easier, we also generate some content visible to browser users. Normally your app content would go there.
const listingUrl = `/listing/${listingId}`
const isLocalhost = APP_BASE_URL.includes("localhost")
return (
<>
{/* These elements will generate the Frame <meta> tags */}
<FrameContainer
pathname={`${listingUrl}`}
postUrl={`${listingUrl}/frames`}
state={state}
previousFrame={previousFrame}
>
<FrameImage src={imageUrl} aspectRatio="1:1" />
<FrameButton>
{errorMessage ||
`Like cast to mint "${title}" (${listing.quantity_remaining}/${listing.quantity_listed})`}
</FrameButton>
</FrameContainer>
{/* This is the content visible to browser users */}
...
Try your frame locally
Start the local server executing:
npm run dev
Let’s say the listing identifier is a414b244-9174-4ab5-9ac5-f366b9d48307
, your local frame URL is: http://localhost:3000/listing/a414b244-9174-4ab5-9ac5-f366b9d48307
You can also view it in the debugger by opening the following link:
http://localhost:3000/debug?url=http://localhost:3000/listing/a414b244-9174-4ab5-9ac5-f366b9d48307
The frames.js debugger has a few cool features. Among others, you can impersonate an arbitrary user and, especially relevant to our project, you can mock hub states such as whether the requester has liked the cast.
Next.js can take a while to compile some routes the first time. If you get error messages on the frame validations because of the timeout, simply refresh the page and try again.
Here is a video that shows how our frame should behave:
Deploy your frame
The last step is to deploy your frame to a public host, so that Farcaster can access it.
Congrats, you’ve completed the tutorial and created your first Farcaster Frame powered by the Phosphor APIs!
Conclusion
Today, we learned a lot! But remember, this is just the beginning of the journey. You can sign up for early access to Phosphor and be among the first to explore its capabilities.
So, let your creativity flow, have fun, and build awesome frames with the Phosphor APIs!
Ideas for improvements
Enhance the user experience by displaying feedback messages (errors, instructions, etc.) as overlays on the image or as separate frames.
Create a frame that shows the payment intent status using the GET /purchase-intents endpoint and the payment intent identifier you got after creation.
Generate a button that redirects to the transaction status on Etherscan (or similar for the network you’re on) using the endpoint GET /transactions to get the
tx_hash
.Require the user to follow a specific account to mint. You can check whether the requester is following the account that posted the frame using
frameMessage.requesterFollowsCaster
, but you might want to require following the original artist's account, for example.Hint: use your preferred hub's Links API or equivalent in other services. For example, to check whether fid 1 follows fid 2 using Pinata Hub: https://hub.pinata.cloud/v1/linkById?fid=1&target_fid=2&link_type=follow