Blog home

How to Use the Frame Development Kit to Build Farcaster Frames

Justin Hunter

Published on

9 min read

How to Use the Frame Development Kit to Build Farcaster Frames

And deploy a frame on Express with Pinata and Railway

We’ve been building a lot of frames, and we’re not alone. It seems like the developers in the Farcaster community are shipping frames by the minute. What are frames? Frames are mini apps that live inside the social feed of Farcaster clients. Farcaster, as we’ve written about before, is a decentralized social protocol. Any app developer can build their own client, and now any developer can build frames for clients that support them.

Frames are an exciting primitive for developers, so in addition to writing tutorials (like this one), we’ve built tools to make it easier to build. One of those tools is the Pinata Frame Development Kit (FDK). This kit is a cleverly named SDK that makes it easy to build frames in JavaScript. In this tutorial, we’re going to recreate the Magic 8 Ball frame that I built, and we’re going to use the FDK.

Getting Started

For this tutorial, you’ll need to store images somewhere, and keeping with the “sufficiently decentralized” concept that powers Farcaster, it makes sense that the images are also decentralized. IPFS is the perfect solution for all Farcaster related media. So, you’ll need a free Pinata account.

Frames, at the end of the day, are server-side code that return HTML. So we’re going to need to build a server and host it somewhere. For this guide, we’ll use Railway, which has a simple to use deployment process and a generous free tier. Sign up for your free Railway account here.

Outside of those two accounts, you’ll also need to be comfortable with your code editor and your terminal.

Creating the Server

Now that we have the accounts we need, let’s get our server spun up. We’re going to build our server using Node.js and Express. To keep this high-level, we’ll use JavaScript instead of Typescript. The Pinata FDK is written in Typescript, and has type support out of the box, so Typescript developers don’t need to worry.

Fire up your terminal, and in the directory where you keep your projects, let’s create a new folder for this one.

mkdir express-frame && cd express-frame

Next, we’ll use a simple CLI tool to create the foundation for our Express app:

npx express-generator --view=none

This will create a basic Express server. You’ll still need to install dependencies. You can do that by running:

npm install

Next, we’ll install a package to help us manage environment variables, a CORS package to make sure all clients that support frames can call our server, and the Pinata FDK to make our frame developing lives easier:

npm i pinata-fdk dotenv cors

Now, we’re ready to code! Open up the project in the code editor of your choice. You can delete the views folder entirely, as we won’t need it for this. You’ll notice two routes were created for us by the generator, the root route and the /users route. We’ll keep this simple and use just the root route, so delete the routes/users.js file. Then, go into the app.js file and remove the references to the users route. We also want to be explicit in the port we bind our server to. This will be necessary when we deploy to Railway. Your app.js file should look like this when you’re done:

const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const cors = require("cors");
require("dotenv").config();

const port = process.env.PORT || 3000;

const indexRouter = require('./routes/index');

const app = express();

app.use(logger('dev'));
app.use(express.json());
app.use(cors());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
});

module.exports = app;

We can start our server now, but it’s not going to do much. Let’s change that. Open up the routes folder and edit the index.js file. For our frame to work, we need to support both the GET and the POST method on our endpoint. Let’s start with GET, since that’s what will be used to first render our frame.

You’ll now need to create images that represent each answer in the Magic 8 Ball might return. Once you’ve created each image, upload them to your Pinata account using the web app. Once you’ve done this, you’ll need to copy each CID because we’ll use them in the code that follows. You’ll also need a starting image. In the example I built, we had the Pinnie logo as our first image. You can choose whatever you’d like, but you’ll need to upload that and grab its CID as well.

Now, let’s add some code. In your routes/index.js file, add the following:

const express = require('express');
const { PinataFDK } = require("pinata-fdk");
const router = express.Router();
require("dotenv").config();

const fdk = new PinataFDK({
  pinata_jwt: "",
  pinata_gateway: process.env.GATEWAY_URL
},
);

router.get('/', async function(req, res, next) {
  try {
    const startingImageCid = "" // PUT YOUR STARTING IMAGE CID HERE
    const frameMetadata = await fdk.getFrameMetadata({
      post_url: `${process.env.HOSTED_URL}`,
      input: { text: "Ask a question" },
      aspectRatio: "1.91:1",
      buttons: [
        { label: 'Shake the 8 Ball', action: 'post' }
      ],
      cid: startingImageCid
    });
    const html = `<!DOCTYPE html>
    <html lang="en">
      <head>
        ${frameMetadata}
      </head>
      <body>
        <div style="background: #fff;">
          <img style="width: 50%; margin: auto;" src="${process.env.GATEWAY_URL}/ipfs/${startingImageCid}" />
        </div>
      </body>
      </html>`
    res.send(html);
  } catch (error) {
    console.log(error);
    res.status(500).send("Server error");
  }
});

module.exports = router;

At the top of the file, we’re importing our dependencies including the Pinata FDK and the dotenv library. We initialize the FDK towards the top of the file. If we were uploading images to our Pinata account using the FDK, we would need to grab an API key and add it in the pinata_jwt value, but since we’re just making use of the gateway and the Farcaster code, we can leave that empty. The gateway URL is mapped to an environment variable we’ll need to add to a file in a moment. We will also create a HOSTED_URL variable so that we can track our server’s URL and use it in our frame’s POST requests.

In the router’s GET method, we are setting our starting CID. Remember you’ll need to upload this image and add the CID as the variable’s value. Then, we are using the FDK to get the frame metadata. The metadata is the HTML meta tags that are added to the HTML we will return. It can be tedious writing these tags, and the FDK makes it easy. In our case, we want a button and a text input as part of our frame. You can do a whole lot more with the FDK, so I recommend looking at the docs.

Finally, we return our HTML. Note that the body element is not strictly necessary, but I like to include it in case people click through on a frame, they’ll have an app experience outside of the frame.

Ok, before we move on to the POST method, let’s get our .env file set up. First, create a .gitignore file and add the following:

./node_modules
.env

This will ensure you don’t commit your environment variables to Github or wherever you host your code. Next, create a .env file and add the following:

GATEWAY_URL=YOUR GATEWAY URL
HOSTED_URL=http://localhost:3000

When we deploy this server, we’ll set a production version of the HOSTED_URL that points to the URL Railway generates for us.

Ok, now we can write our POST method. Add the following below the GET route:

router.post('/', async function(req, res, next) {
  try {
    const answerCids = [];
    const selectedAnswer = answerCids[Math.floor(Math.random() * answerCids.length)];
    const frameMetadata = await fdk.getFrameMetadata({
      post_url: `${process.env.HOSTED_URL}`,
      input: { text: "Ask a question" },
      aspectRatio: "1.91:1",
      buttons: [
        { label: 'Shake the 8 Ball', action: 'post' }
      ],
      cid: selectedAnswer
    });
    const html = `<!DOCTYPE html>
    <html lang="en">
      <head>
        ${frameMetadata}
      </head>
      <body>
      <div style="background: #fff;">
        <img style="width: 50%; margin: auto;" src="${process.env.GATEWAY_URL}/ipfs/${selectedAnswer}" />
      </div>
    </body>
      </html>`

    if (req.body?.trustedData?.messageBytes) {
      console.log(req.body);
      const { isValid, message } = await fdk.validateFrameMessage(req.body);
      console.log(isValid);
      if (isValid) {
        //  Do something interesting with the data
      }
    }
    res.send(html);
  } catch (error) {
    console.log(error);
    res.status(500).send("Server error");
  }
});

The first thing you’ll notice in the POST method is a variable for answerCids. This is where you will copy and paste each of your answer CIDs. Remember, these are the CIDs that point to each image of the Magic 8 Ball answers that you created. From that array, we then randomly select one as the answer that we’ll return to the frame.

Just like with our GET method, we’ll use the FDK to generate our metadata. The difference is that we use the randomly selected CID as the image CID. Below our HTML, we can validate the signature that is sent when people interact with frames. This validation allows us to ensure that the person indicated in the request body actually is the person who interacted with the frame. If the signature is valid, you can trust the data and do something with it, such as store it in a database if it makes sense for you.

And with that, we’ve written all the code we need to write. The FDK really did reduce the number of lines of code we needed to write. Let’s test this. I’m going to update the package.json to run our server with a simple node command like this:

"start": "node app.js"

Run your server by opening our terminal and executing this command:

npm run start

In another terminal window, let’s use curl to test both the GET and POST endpoints. Run this command:

curl localhost:3000

You should see the correct HTML returned. Now, run this command to make a POST request:

curl -X POST localhost:3000

You should, once again, see the correct HTML returned. And that’s all there is to it. We wrote our Magic 8 Ball frame. Next up, we’ll deploy it.

Deploying

As mentioned at the beginning, we’re going to use Railway because they provide a really intuitive service and have a nice free tier. To use Railway, we’ll want to deploy from Github. So, create a Github account if you don’t have one, then initialize your local code with a git repository by running this command:

git init

Next, create a repo in Github and follow the steps to push your local code to the remote repo. With that done, we can get to work on Railway. While signed into your Railway account, click the ‘New Project’ button. You’ll see a screen like this:

Choose the ‘Deploy from Github’ repo option. Next, you’ll be given the option to “Configure app.” This will allow you to select the repo to deploy from. Follow the steps to kick off the deployment process. After the server is deployed, click on the service, and then click the ‘Settings’ tab. From there, you can generate a domain for your server.

Once the domain is generated, you can use it to test your frame. Go to the Warpcast Frame Validation tool and paste your server URL in to verify it all works. There’s helpful info in that tool, just in case your frame doesn’t work, but you should be good to go.

Now, you can cast your server URL and let the world interact with your newly created frame!

Conclusion

This tutorial focused on replicating a frame that already exists, but hopefully it’s enough to get you started on building frames with the Pinata FDK. The Pinata FDK makes it simple to build frames in all Node.js environments.

Happy pinning and happy framing!

Stay up to date

Join our newsletter for the latest stories & product updates from the Pinata community.

No spam, notifications only about new products, updates and freebies. You can always unsubscribe.