Back to blog
How to Build Farcaster Frames In Go
Frames are self-contained applications that run in-feed on the decentralized social protocol Farcaster. Any client (application) building atop Farcaster can support frames, and when the client supports frames, it means developers get instant distribution of their app or game in frame format.
This new paradigm has led to an explosion of creativity from developers, and most of that creativity has been centered around the world of JavaScript. But frames are language agnostic. All you need is a server that can return HTML for a frame to work. Paulo Madronero, an engineer here at Pinata, proved this by building this incredible frame in Go.
Today, we’re going to walk through building a frame in Go like Paulo did. So buckle up, Gophers. We’re going to show those JavaScript nerds (basically, me) how to do things in Go. We’re going to build a meme fetcher for our frame example.
Getting Started
To start, you’ll need to make sure Go is installed on your machine. Because of the various operating systems, I’m going to just link to the official documentation for installing and running Go programs. You can find those docs here.
You’ll also want to sign up for a free Pinata account here. We’ll use Pinata’s free Farcaster Hub. As you iterate on your frame development, you may want to upload your own images, and Pinata is a great solution for hosting these images on IPFS.
To host the code for this server, we’re going to use Github. If you don’t have a Github account, you can sign up for free here.
Finally, we’re going to deploy this app on Render, a great hosting platform for backend code. So, you’ll want to sign up for a free Render account here.
Now that you have all your accounts set up, let’s write some code!
Creating the API Routes
We’re going to make use of Go’s built-in HTTP functionality to spin up a server for returning our frame HTML. So, it makes sense to start there before writing any of the frame logic. In your command line program, create a new project folder and change into that director:
mkdir go-frame && cd go-frame
Now, let’s initialize our Go project:
go mod init main.go
This will create a go.mod
file that points to your main project. If you open your project, you’ll see that file but nothing else. Let’s create a main.go
file to correspond to what we just did when we initialized the project. Inside that file, let’s get the framework for our HTTP API created like so:
package main
import (
"fmt"
"net/http"
)
func main() {
// Handle requests on the root path
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Set content type to HTML
w.Header().Set("Content-Type", "text/html")
// Check the request method and respond accordingly
switch r.Method {
case "GET":
fmt.Fprintf(w, "This is a response to a GET request")
case "POST":
fmt.Fprintf(w, "This is a response to a POST request")
default:
// Respond with a 405 Method Not Allowed if the method is not GET or POST
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintf(w, "405 Method Not Allowed")
}
})
// Start the HTTP server on port 8080 and handle errors
fmt.Println("Server is running on http://localhost:8080")
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println("Error starting server: ", err)
return
}
}
This code uses Go’s built-in net/http
package as well as its built-in fmt
package. It creates a simple server that will respond to GET and POST requests on the main / route. In a more complex program, you might have multiple routes like /frame1
, /frame2
, etc. but we’ll keep this simple.
To test our program, run the following from the terminal:
go run main.go
You should see the message “Server is running on http://localhost:8080” printed in your command line program. Open your browser to that URL and you’ll see:
This is a great start, but it’s not a frame yet. We need to return HTML with the correct meta tags. You can read more about the Frames Spec here. To return dynamic HTML, we’re going to make use of another built-in Go package called html/template
. The way this package works is it allows you to write HTML in a standalone file with placeholders for variables that will be set at runtime. If you’re familiar with other SSR (server side rendered) frameworks like Next.js, this is essentially the same thing, but in Go.
Let’s update our main.go
file to look like this:
package main
import (
"fmt"
"html/template"
"net/http"
"path/filepath"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// Parse templates
templatesDir := "path/to/your/templates"
getTemplate := template.Must(template.ParseFiles(filepath.Join(templatesDir, "getTemplate.html")))
postTemplate := template.Must(template.ParseFiles(filepath.Join(templatesDir, "postTemplate.html")))
// Handle requests on the root path
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Set content type to HTML
w.Header().Set("Content-Type", "text/html")
// Check the request method and serve the appropriate template
switch r.Method {
case "GET":
if err := getTemplate.Execute(w, nil); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
case "POST":
if err := postTemplate.Execute(w, nil); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
default:
// Respond with a 405 Method Not Allowed if the method is not GET or POST
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintf(w, "405 Method Not Allowed")
}
})
// Start the HTTP server on port 8080 and handle errors
fmt.Println("Server listening on port", port)
httpError := http.ListenAndServe(":"+port, nil)
if httpError != nil {
fmt.Println("Error starting server: ", httpError)
return
}
}
You’ll immediately notice we’re defining a variable that points to a path for our templates directory, but we don’t have that yet. We’ll create one next, but let’s walk through the rest of this code. We’re still checking for either GET or POST requests to ensure we return the correct template. We’re using the html/template
package to read and execute the HTML template specific for each request type. And we’re handling errors. Pretty straightforward.
Let’s create a directory at the root of our project called templates
. In that directory, you’ll add two files:
getTemplate.html
postTemplate.html
To start, let’s just add some simple HTML in each of these file like so:
<!DOCTYPE html>
<html>
<head>
<title>GET Request</title>
</head>
<body>
<h1>This is a response to a GET request</h1>
</body>
</html>
You can change the GET references to POST in the postTemplate.html
. Now, if you restart your program, you can use curl to make GET and POST requests to test this. Here’s an example for the GET request to run in another command line program window:
curl localhost:8080
You’ll see the HTML from your GET template rendered. Now, run this in your command line program:
curl -X POST localhost:8080
Now, you’ll see the results from the POST template.
This, of course, is all very basic, but it’s laying the foundation for building our frame. We will make use of dynamic templating in our HTML templates to render various frames. Let’s get into it!
Building The Frames
To build our frame templates and the logic for passing in dynamic variables, let’s go back to our original goal. We’re going to build a frame that allows those that interact with it to fetch a random meme from the memes channel on Farcaster. Channels are where groups of similarly related casts (posts on the Farcaster network) can be aggregated, similar to Reddit channels.
To do this, we will need a function that grabs a random meme from the memes channel and displays it both as the first frame and every time the user clicks the Get New Meme button.
So, let’s start by writing that function that will make an API request to a Farcaster Hub (we’ll use the Pinata Hub). The function is relatively simple, but we also need to define structs for our data. So let’s get that all done above the main
function in our file like so:
type Embed struct {
URL string `json:"url"`
}
type CastBody struct {
EmbedsDeprecated []string `json:"embedsDeprecated"`
Mentions []int64 `json:"mentions"`
ParentUrl string `json:"parentUrl"`
Text string `json:"text"`
MentionsPositions []int16 `json:"mentionsPositions"`
Embeds []Embed `json:"embeds"`
}
type Data struct {
Type string `json:"type"`
FID int64 `json:"fid"`
Timestamp int64 `json:"timestamp"`
Network string `json:"network"`
CastAddBody CastBody `json:"castAddBody"`
}
type Messages struct {
Data Data `json:"data"`
Hash string `json:"hash"`
HashScheme string `json:"hashScheme"`
SignatureScheme string `json:"signatureScheme"`
Signer string `json:"signer"`
}
type MemeResponse struct {
Messages []Messages `json:"messages"`
NextPageToken string `json:"nextPageToken"`
}
func GetRandomMeme() string {
url := "https://hub.pinata.cloud/v1/castsByParent?url=chain://eip155:1/erc721:0xfd8427165df67df6d7fd689ae67c8ebf56d9ca61"
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Error making the request: %v\n", err)
return "Error"
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Error reading response body: %v\n", err)
return "Error"
}
var apiResponse MemeResponse
if err := json.Unmarshal(body, &apiResponse); err != nil {
fmt.Printf("Error parsing JSON response: %v\n", err)
return "Error"
}
var matchingURLs []string
for _, message := range apiResponse.Messages {
for _, embed := range message.Data.CastAddBody.Embeds {
if strings.HasSuffix(embed.URL, ".png") || strings.HasSuffix(embed.URL, ".jpg") || strings.HasSuffix(embed.URL, ".gif") {
fmt.Println("Found matching URL:", embed.URL)
matchingURLs = append(matchingURLs, embed.URL)
}
}
}
if len(matchingURLs) == 0 {
fmt.Println("No matching URLs found")
return "Error"
}
randomIndex := rand.Intn(len(matchingURLs))
selectedURL := matchingURLs[randomIndex]
fmt.Printf("Selected meme: %+v\n", selectedURL)
return selectedURL
}
The struct types that are defined at the top are based on the response format for raw Hub requests to the getCastsByParent
endpoint we’re using. We use those definitions to help us parse the unmarshalled JSON data from the response body. Meme photos posted to the memes channel will always have a file extension at the end, and we know the supported file extensions are .png
, .jpg
, and .gif
. So, we are filtering for just urls in the embeds array that have those extension. We then assign results to a string array variable called matchingURLs
. Finally, we select a random URL from the array to return.
Now, this code needs a few additional built-in Go packages, so update your import statement at the top to look like this:
import (
"encoding/json"
"fmt"
"html/template"
"io"
"math/rand"
"net/http"
"path/filepath"
"strings"
)
Now, with that function written, we just need to wire it up to our main HTTP API function. In our HTTP handler, we’re going to fetch the meme URL. You can place this code right beneath where we set the content type header.
randomMemeUrl := GetRandomMeme()
if randomMemeUrl == "Error" {
fmt.Println("Error getting meme")
return
}
We don’t have anywhere to use this random meme URL yet, so let’s rectify that. We need to dynamically render and image for our frame template HTML. We’ll use the template variables that come out of the box with Go’s html/template
package.
Our template is actually only going to be dynamic based on the image, not by the request type. We had set up a GET and a POST template, but we can consolidate this down to one template. Choose whichever one you’d like and delete the other. The remaining template should be re-titled template.html
and you can update it to look like this:
<!DOCTYPE html>
<html>
<head>
<title>Memerator 3000</title>
<meta name="fc:frame" content="vNext">
<meta name="og:image" content="{{ . }}">
<meta name="fc:frame:image" content="{{ . }}">
<meta name="fc:frame:aspectRatio" content="1.91:1">
<meta name="fc:frame:button:1" content="Load a random meme">
</head>
<body>
<img src="{{ . }}" />
</body>
</html>
With the templating language, we can do a lot of powerful things, but all we really need is to dynamically update the image URLs in various places in our HTML. The {{ . }}
represents the dynamic URL we will pass in. The rest of this HTML is standard according to the frame spec. The button in the frame will load another random meme.
Now, let’s update our API code to render the new single template and pass in the dynamic meme URL. Find the switch statement that handles the GET and POST requests and update it to look like this:
switch r.Method {
case "GET":
if err := template.Execute(w, randomMemeUrl); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
case "POST":
if err := template.Execute(w, randomMemeUrl); err != nil {
http.Error(w, "Failed to render template", http.StatusInternalServerError)
}
default:
// Respond with a 405 Method Not Allowed if the method is not GET or POST
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintf(w, "<h1>405 Method Not Allowed</h1>")
}
})
The differences here are that we are rendering the same template for both GET and POST requests. We technically could have removed the switch statement and just always returned the template but keeping it like this allows the code to be easily extended if you want to rendered a completely different template for POST or GET methods.
The other change is that when we execute the template, we are passing in our dynamic variable: randomMemeUrl
.
Let’s test it! Restart your server and open your browser to localhost:8080
. You should see a random meme. Refresh, and you’ll get another one.
We can test the POST endpoint using curl again. We want to make sure, specifically, that the head tags are set properly. So once more, run the following in your command line program:
curl -X POST localhost:8080
You should see, each time you run that command, a new image URL generated in all the places we set in our Go HTML template.
Deploying and Testing
Frame validation is a little difficult to do locally. What we did with the curl commands is local testing and we can see that everything should work. But to know for sure, we need to deploy this thing.
Let’s first get it pushed up to Github because we’ll use the public Github repository to deploy from on Render. Log into Github and create a new repository. Then, back in your command line program, run the following:
git init
git add .
git commit -m "initial commit"
Then follow the instructions on your new git repo on Github to push to your local repository to the remote repo on Github.
Once you’ve done that and your repository is publicly hosted on Github, it’s time to spin up a Render server. Once signed into Render, click the New button in the top-right and choose web service.
Next, choose Build and deploy from a git repository.
Next, you’ll need to connect your Github account and choose the repository you created for the Go frame. Once you’ve done that, you’ll see a page like this:
Outside of giving the service a name, you should be able to leave everything as default. Then, choose the free option below and click Create Web Service at the bottom. It will take a few minutes to build and deploy the app, but when it’s done, you’ll receive a URL for your web service. This is what we will use to test.
The service may fail to deploy because of port binding. If this happens, go to the Environment tab for your service and add a new variable called PORT and point it to 8080
.
You’ll need to redeploy after that, and then everything should be working. You can do a quick test by visiting the URL generated by Render. Just like when we tested locally, you should be able to reload the page and get a new meme each time.
But we really want to test via Farcaster. Luckily, Warpcast (a Farcaster client built by the Farcaster team) has a frame tester. You can visit this link and enter your frame app URL to test it as if it were the full frame experience. It should look something like this:
Assuming it’s working, you’re ready to send your frame out into the world. You can post the link to your frame app in any Farcaster client that supports frames such as Warpcast or Supercast.
Conclusion
While most of the frames built to date have been built in JavaScript and hosted on Vercel, it’s possible to build frames in any language and host on various web server hosting solutions. Between Paulo’s Go-example and this tutorial, hopefully you’re ready to start experimenting with Go-based frames. If you want the full source code for this project, you can find it here. Now, it’s up to you to extend your own frames into something fun and interesting.