Tudor (two-door)
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.
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.
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.
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.
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
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 "MVP or Not MVP" here: https://warpcast.com/tudorizer/0xd122d681
Checkout the repo: https://github.com/tudormunteanu/frames-demo-1/
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.
Next.js 14
React
Vercel Cloud hosting + CI/CD
Satori + Sharp
Neynar for access to a Farcaster Hub
TypeScript
yarn
The floor is lava: https://warpcast.com/pplpleasr/0xe21ad88f
Top Frames in Launchcaster: https://www.launchcaster.xyz/?sort=top&text=frame
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.
ImageResponse
which uses satori
under the hood?That's just syntactic sugar.
<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.
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.
Frame Review #4: Addresso by @tudorizer @addresso /addresso https://my.addresso.com
i love this format, what are your favourite frames? i'm looking to get in touch with great frames builders!
👋
hello! can you share some frames you built?
when you added a first entry through a frame, was this on mobile or on desktop?
desktop
thanks. I'm trying to replicate this, yet failing :(
Thanks @jake for taking the time to both check out Addresso AND provide feedback 😍. We crave feedback as you might just guess 😀 and I'd love to pick up on a few of your observations. The thinking behind the button order is simply that no-one will sync if they're not returning, but someone with an existing Addresso Book might just click "Add" without first syncing, which would leave them with separate Addresso Books, which most definitely is not the idea! Will give this some more thought.
perhaps "Create a new address book" and "I already have one" as simple 2 button options to start?
Oooh nice. Saying that, we don't separate "Create a new address book" from "Add an entry". An address book is created WHEN the first entry is added. We think it's quite possible that no human being has ever thought "Oh yeah man, I need to create an address book" 😂 ... but questions such as "How and where can I store this address?" are necessity driven.
We had "Contact" rather than "Entry" for a while pre-launch, but it felt very far from future proof. Or indeed, wholly relevant in the here and now. Addresso helps you store and organize blockchain addresses. That's all variety, not just those that relate to a contact, e.g. your own wallets, smart contracts, AI agents, ... We need words that already mean something for their communication power. And words that are also suited to this new technological paradigm. Sometimes it seems we spend as long thinking about the lexicon as writing the code!! What do you think?
Maybe "address" then? Not a huge deal. I think "contact" is good because it's familiar and "entry" makes sense as future-proof and neutral, but you can always change it. Brand name should be stickier, though of course you can change that as well, but the reasons you decided to go for Addresso (I like the name) can probably be applied to support using "address" for the term
Re. "Sign & Save", I think you worked this one out for yourself. Although it shouldn't confuse in the first place of course. On us. Signing is essential to give you the warm glow of knowing it's exactly what you think it is even though you've not used the address in question it in six months. Safe. Secure. But because it's a bit of effort, it makes sense to allow you to make many additions and/or edits before signing them all the once.
> send us a representative sample of your work
😂
500 $degen 🍖x100
Did I get the job? 😄
this is how it's done 👍 is it the fabled yonfrulizer?
Yup, I just need to figure out how to make a frame server now!
Would this help? https://paragraph.xyz/@tudorizer/not-your-average-frames-tutorial
if you needed help with that, let me know