Intro
Let’s build a Farcaster Frame!
After reading this tutorial, you will be have an understanding of the main components driving a Frame and see them in action.
As content for this tutorial I assembled a little trivia-style game: MVP or Not MVP. The objective is to guess whether a product or service were MVPs when publicly announced. Players select one of two options and at the end receive a score based on the number of correct answers.
Frames are rather simple primitives and after reading the official spec of a Frame, you might get a sense of what they are and why they unlock a new level of interactivity inside social feeds. They extend the OpenGraph protocol by adding dynamic content on top of what was meant to standardise static metadata about web pages.
How do you build one?
Some familiarity with Next.js is encouraged, although you could simply clone the repo, run it and tinker with the code, a lot of less obvious conventions are imposed by the framework.
Along the way, I will explain some fundamentals and lessons learned.
Architecture
This demo has the following components:
The Frame front-end - to render URLs with Frame meta tags
The Frame Server - to handle taps on buttons and advance the game
Persistent Storage for the game state - to store the game across sessions
A Farcaster Hub - to validate FIDs and message data
When Farcaster clients try to embed the content of an URL, they look at all the <meta>
tags in the <head>
. If clients find a set of tags that correctly define a Frame, they render it, otherwise they fall back to the OG protocol.
The first rendered frame is cached, so the best practice is to not have dynamic data on it.
When users tap any of the Frame buttons, Farcaster makes a request to the Frame Server to perform some logic and return the content of another Frame.
The URL of the Frame Server is defined by the <meta property="fc:frame:post_url" content="..." />
tag.
Step 1 - Create a first static Frame
This is almost trivial given the generateMetadata
functionality in Next.js 14, because inside this function we only need to return an object with the right keys.
Here’s my example:
fc:frame:post_url
and fc:frame:image
point to the Frame Server, which for this Next.js app is hosted on the same machine as the front-end.
This Frame contains a single button, with default actions. Tapping it makes a POST request to /api/start
with info about the user who tapped and index of the button.
Step 2 - Responding to the button
This logic sits server-side on the Frame Server, so it has access the persistent storage service to initiate the game.
Source at /app/api/start/route.ts
The Request object is key here, because it contains a payload sent by Farcaster with an encoded version of the message, the FID of the user who triggered the action and the URL of the frame. These can be validated by a Farcaster HUB. I use Neynar in this demo. The validation logic itself is fairly straight-forward and lifted from the official Frames demo. See it inside app/frames.ts.
To separate display from business logic, the endpoint completes with a 302 redirect to the page which renders a new Frame and new buttons.
The first screen had a single button, so there’s no need to check its index, because it can only have one function: start the game.
After the redirect to /start
a frame renderes with the first level:
So far we covered 3 paths:
/ (root path)
/api/start
/start
These 3 paths cover the core loop of any Frame:
Render
Interact
Call Frame Server
Repeat
To chain multiple levels together, I split the game in 3 stages:
/start
- the first screen which starts the game/next
- levels 1 through 5/over
- the final screen
Each stage is comprised of an API endpoint plus a corresponding Next.js page.
Diving into /next
as an example, this is the API endpoint:
Source from /app/api/next/route.ts
Assuming the game did not end, the code to render the next level Frame looks like this:
Source from /app/next/page.tsx
The API endpoint passes query params to the Next.js page it redirects to. Here, the game id is sent so the page knows which image to display.
Next.js runs the above code client-side, so we won’t have access to server-side functionality like Storage.
Step 3 - Rendering Images
So far the app takes advantage of mostly Next.js boilerplate code, while following the Frame specs. The final challenge is rendering images for the frames. Fortunately, there are quite a few options available:
Here's a glance over the code:
Source app/api/images/level/route.tsx
Step 4 - Recap
The cycle is complete once the image for a new frame is rendered. Forks in the logic path can be added and the flow expanded path this point. We can now build navigation for a tiny, focused app.
That was it! This minimal game emphasizes the best parts of building Farcaster Frames.
Constraints are great for innovation as they provide focus, encourage creativity, foster resourcefulness, create urgency, x promote efficiency. These limitations encourage innovators to concentrate on specific problems, think creatively, utilize resources effectively, act swiftly, and seek efficient solutions. The key lies in approaching them with a positive and creative mindset, turning challenges into opportunities for breakthroughs.
In times when the UI stack for building web apps is getting more and more complex, Frames feel like a fresh take, back to first principles of interaction, natively supported on most platforms.
If Next.js is too much and because most of the content is rendered on the server, you can choose almost any other framework, regardless of language. I was tempted to replicate this demo in Flask, FastAPI or Hono.
Play!
Play "MVP or Not MVP" here: https://warpcast.com/tudorizer/0xd122d681
Checkout the repo: https://github.com/tudormunteanu/frames-demo-1/
FAQ
▼Why did you not use frames.js, onchainkit or frog.fm?
Frames.js , frog.fm and onchainkit are great libraries that abstract away a lot of boilerplate code, while introducing good practices. I decided to not use them for the purpose of this demo, to highlight some fundamentals that app frameworks like Next.js already include.
My intention was to keep dependencies to a minimum, to focus on the core.
▼What is the full tech-stack used in the demo?
Next.js 14
React
Vercel Cloud hosting + CI/CD
Satori + Sharp
Neynar for access to a Farcaster Hub
TypeScript
yarn
▼Where can I find other interesting frames?
The floor is lava: https://warpcast.com/pplpleasr/0xe21ad88f
Top Frames in Launchcaster: https://www.launchcaster.xyz/?sort=top&text=frame
▼What is the version constant and why use it?
At the moment, the version is appended to relevant URLs to bust the response cached by Farcaster. It does it by appending a unique query param to the URL (classing web2 pattern). The value can be anything unique and I chose a timestamp for simplicity.
This is particularly useful if used to version releases of your Frame, manually or as part of a CI/CD pipeline.
▼Why not use the ImageResponse
which uses satori
under the hood?
That's just syntactic sugar.
▼What are all the possible <meta>
tag properties relevant to building a Frame?
The minimum required properties are:
fc:frame (currently can only be vNext
)
fc:frame:image
og:image
The minimum required properties are:
fc:frame (currently can only be vNext
)
fc:frame:image
og:image
Other optional properties are:
fc:frame:button:$idx
fc:frame:post_url
fc:frame:button:$idx:action
fc:frame:button:$idx:target
fc:frame:input:text
fc:frame:image:aspect_ratio
fc:frame:state
Find the full spec with explanations in the official Farcaster docs.
See also
The state of the Open Graph protocol doesn't seem to be great. Twitter has cards, while Google went with another standard from schema.org.
While the core value prop is simple: "encourage websites to include metadata which tells other platforms what they should look at", the value prop of similar standards might soon be challenged by AI, given the relatively narrow and content driven scope of the intention.
I'm hopeful this enhancement of the standard will live on at Farcaster and improvements like Frame Transactions take it to new heights. Building it in public, sufficiently-decentralised and outside the tutelage of one entity might lead to a different fate.
If you have further questions or need additional clarification on any of the content discussed in this tutorial, don't hesitate to reach out on Warpcast, Twitter, LinkedIn or GitHub. I'm always open for conversations and eager to assist!
Remember, learning is a journey, not a destination. Let's keep exploring together.
Thanks to everyone who reviewed this article, gave feedback and tips. Particularly Tommy and Mark.