How to Build & Deploy a simple Farcaster Frame

Dhaiwat PandyaDhaiwat Pandya
6 min read

If you're reading this, you're likely already curious about Farcaster Frames. For those who aren't familiar, start with a quick overview at the Farcaster Frames Documentation.

What We Will Build

In this tutorial, we will be building a frame that lets users flip a coin and see the result. Users can flip the coin multiple times.

Fairly simple.

This is the rough lifecycle of the 'app':

And this is how it will look:

Step 1: Initialize Next.js Project

You've probably done this a thousand times before. We're gonna use the App Router, so make sure you select that in the CLI when prompted.

pnpm create next-app

Step 2: Install Dependencies

To make it easier to work with the Farcaster Frames Standard, we will be using some utilities provided by the @coinbase/onchainkit npm package. Install this dependency.

pnpm add @coinbase/onchainkit

That's the only dependency we will need for this project.

Start the Next.js dev server now too:

pnpm dev

Step 3: Expose Localhost To The Internet

Unfortunately, there are no tools that let you test out & debug your frames locally yet. Our best bet is to use the Warpcast Frame Validator.

In order to debug a localhost site using the Warpcast Frame Validator, we will have to expose our localhost to the internet. I use ngrok for this. Once you sign up, you will find instructions for how to setup ngrok on their dashboard. Just follow them, and use it to expose your port 3000. It will be a command like this:

ngrok http --domain=example-domain.ngrok-free.app 3000

Step 4: Setup Your .env.local File

Create a .env.local file at the root of your project, and add these contents to the file:

File: .env.local

NEXT_PUBLIC_BASE_URL=https://example-domain.ngrok-free.app

Set the NEXT_PUBLIC_BASE_URL env variable to your ngrok URL.

We will need this env variable in our project.

Step 5: Setup The Assets

Each Farcaster Frame requires an image that is presented in the UI by the clients. We will need three different images for our frame. One for the initial 'splash' screen, one for when the result of the coin flip is heads, and one for when the result is tails.

You can either use your own images, or copy over the ones I already created from this folder: https://github.com/Dhaiwat10/coin-flip-farcaster-frame/tree/main/public. The ones you need are splash_image.png, heads.png, and tails.png. Put these inside of your public folder.

Step 6: Build The Initial Frame

Our initial frame will look like this:

There are two things here: an image, and a button.

Most frames are as simple as that. Let's code this out.

According to the Farcaster Frames specs, a URL is considered a valid Farcaster Frame if it returns some certain <meta> tags:

Let's setup these <meta> tags for our initial frame. We will use the getFrameMetadata helper from onchainkit for this task.

File: src/app/page.tsx

import { getFrameMetadata } from '@coinbase/onchainkit';

const NEXT_PUBLIC_URL = process.env.NEXT_PUBLIC_BASE_URL;

const frameMetadata = getFrameMetadata({
  buttons: [
    {
      label: 'Flip Coin!',
    },
  ],
  image: `${NEXT_PUBLIC_URL}/splash_image.png`,
  post_url: `${NEXT_PUBLIC_URL}/api/frame`,
});

Now, we will return this metadata from our page so that whenever someone makes a GET request to our / route, a valid Farcaster Frame is returned.

File: src/app/page.tsx

import { getFrameMetadata } from '@coinbase/onchainkit';
import type { Metadata } from 'next';

const NEXT_PUBLIC_URL = process.env.NEXT_PUBLIC_BASE_URL;

const frameMetadata = getFrameMetadata({
  buttons: [
    {
      label: 'Flip Coin!',
    },
  ],
  image: `${NEXT_PUBLIC_URL}/splash_image.png`,
  post_url: `${NEXT_PUBLIC_URL}/api/frame`,
});

export const metadata: Metadata = {
  title: 'Coin flip frame',
  description: 'Heads or tails? Find out!',
  openGraph: {
    title: 'Coin flip frame',
    description: 'Heads or tails? Find out!',
    images: [`${NEXT_PUBLIC_URL}/splash_image.png`],
  },
  other: {
    ...frameMetadata,
  },
};

export default function Page() {
  return (
    <>
      <h1>Coin Flip frame</h1>
      <p>Simply cast this URL to embed a coin flip in your cast!</p>
    </>
  );
}

Since we are using the Next.js App Router, we can easily set the metadata for our page by exporting a const named metadata.

You might have noticed that we set the post_url field to the /api/frame route on line 13. That's the URL our frame will 'post' to whenever someone clicks our button, i.e. the 'response frame'. Let's build out this route.

Step 7: Build The Response Frame

This is what our Response Frame will look like:

Create a new file at src/app/api/frame/route.ts:

File: src/app/api/frame/route.ts

import { NextRequest, NextResponse } from 'next/server';

const NEXT_PUBLIC_URL = process.env.NEXT_PUBLIC_BASE_URL;

export async function POST(req: NextRequest): Promise<Response> {
    // this is where our logic will go
}

export const dynamic = 'force-dynamic';
  • Exporting a function named POST enables our route to respond to any incoming POST requests.

  • force-dynamic makes sure that no responses are cached.

Let's add in all the logic now, piece-by-piece.

Our first concern is to read the incoming request's data, and make sure that it is a valid request coming from a Farcaster Frame interaction. We want to return a 400 straight away in case it isn't.

File: src/app/api/frame/route.ts

import {
  FrameRequest,
  getFrameMessage
} from '@coinbase/onchainkit';

export async function POST(req: NextRequest): Promise<Response> {
  const body: FrameRequest = await req.json();

  const { isValid } = await getFrameMessage(body, {
    neynarApiKey: "NEYNAR_ONCHAIN_KIT", // you can also your own Neynar API key here
  });

  if (!isValid) {
    return new NextResponse('Invalid request', { status: 400 });
  }
}

Now, let's do the actual coin flip.

const coinFlip = Math.random() > 0.5 ? 'heads' : 'tails';

The last step is to return the result back as a frame. We will also include a button that lets the user flip a coin again if they want!

File: src/app/api/frame/route.ts

import {
  FrameRequest,
  getFrameMessage,
  getFrameHtmlResponse,
} from '@coinbase/onchainkit';
import { NextRequest, NextResponse } from 'next/server';

const NEXT_PUBLIC_URL = process.env.NEXT_PUBLIC_BASE_URL;

export async function POST(req: NextRequest): Promise<Response> {
  const body: FrameRequest = await req.json();

  const { isValid } = await getFrameMessage(body, {
    neynarApiKey: "NEYNAR_ONCHAIN_KIT",
  });

  if (!isValid) {
    return new NextResponse('Invalid request', { status: 400 });
  }

  const coinFlip = Math.random() > 0.5 ? 'heads' : 'tails';

  return new NextResponse(
    getFrameHtmlResponse({
      buttons: [
        {
          label: 'Flip Again!',
        },
      ],
      image: `${NEXT_PUBLIC_URL}/${coinFlip}.png`,
      post_url: `${NEXT_PUBLIC_URL}/api/frame`,
    })
  );
}

export const dynamic = 'force-dynamic';

Since the post_url is set to /api/frame, the same route will be hit again when the user clicks the 'Flip again!' button and the response frame will be returned again.

Step 8: Prod Deployment

Now, you can deploy your Next.js app to Vercel and once you get a deployment URL, set the NEXT_PUBLIC_BASE_URL environment variable on Vercel to your deployment URL. eg. if your Vercel URL is https://example-vercel-domain.vercel.app, then set NEXT_PUBLIC_BASE_URL to https://example-vercel-domain.vercel.app from your Vercel dashboard.

Once you do this, your frame should be perfectly deployed at that URL. Any casts containing this URL will make your frame appear on the feed.

Congratulations, you just built and deployed your first Farcaster frame!

If you found this guide helpful, drop me a follow:

https://warpcast.com/dhai.eth

https://twitter.com/dhaiwat10

Repo For Reference

https://github.com/dhaiwat10/coin-flip-farcaster-frame/

https://github.com/davidfurlong/awesome-frames

1
Subscribe to my newsletter

Read articles from Dhaiwat Pandya directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dhaiwat Pandya
Dhaiwat Pandya

DevRel in web3. Retired 10x engineer. Developer DAO enjoyer