Casting from a frame using signers

In this guide, we'll look at how to build a frame using which people can create casts and perform other actions on the Farcaster app. For this guide, I will be using Next.js and frames.js but feel free to refer to this guide for any other framework with the necessary changes.

For this guide, we'll go over:

  1. Adding sign-in with neynar
  2. Storing signers in a DB
  3. Using the signer to create casts

Before we begin, you can access the complete source code for this guide on GitHub.

Let's get started!

Setting up

App

We're going to use next.js so that we can have everything from our login page to our frame in the same project. So, create a new project using the following command:

npx create-next-app@latest cast-frame-signer

You can choose the configuration based on your personal preference, I am using this config for the guide:

Once the app is created, install the packages that we are going to need for the command:

npm i @neynar/react @neynar/nodejs-sdk axios @prisma/client prisma frames.js
yarn add @neynar/react @neynar/nodejs-sdk axios 
bun add @neynar/react @neynar/nodejs-sdk axios 

Database

We're also going to need a database to store all the signers so that we can later access it to create casts and perform other actions on the user's behalf. I am going to use MongoDB as the database, and prisma as an ORM. So, go ahead and create an account/sign into your MongoDB account here. Once you've signed up go ahead and setup a new cluster on Mongodb. You can follow this guide to do it. Once you've setup your cluster follow these steps to get your connection URL:

  1. Click on Connect in the dashboard tab
  1. In the modal select drivers as the connection method
  1. Copy the connection string from here and replace <password> with your user password

Now, let's go ahead and set up Prisma in our project. Run the following command to initialise:

npx prisma init --datasource-provider MongoDB

Once the initialisation is complete head over to the .env file and add the connection url that you just copied and replace the part after .mongodb.net/ with /db?retryWrites=true&w=majority.

🚧

Make sure to add .env into .gitignore

Firstly, let's first define our database schema. Head over to prisma/schema.prisma and add a user model like this:

model User {
  fid        String @id @default(cuid()) @map("_id")
  signerUUID String @unique
}

Once you have added the model, run these two commands:

npx prisma db push
npx prisma generate

Now, we can add the Prisma client which we'll use to interact with our database. To do that, create a new lib/prisma.ts file in the src folder and add the following:

import { PrismaClient } from "@prisma/client";

let prisma: PrismaClient;

if (process.env.NODE_ENV === "production") {
  prisma = new PrismaClient();
} else {
  const globalWithPrisma = global as typeof globalThis & {
    prisma: PrismaClient;
  };
  if (!globalWithPrisma.prisma) {
    globalWithPrisma.prisma = new PrismaClient();
  }
  prisma = globalWithPrisma.prisma;
}

export default prisma;

Adding sign-in with neynar

Now that we've setup all the boilerplate let's start coding the actual part. We'll first go ahead and add sign in with neynar.

To do that we need to wrap our app in a provider, so, head over to the layout.tsx file and wrap your app in a NeynarContextProvider like this:

"use client";

import { NeynarContextProvider, Theme } from "@neynar/react";
import "@neynar/react/dist/style.css";
import { Inter } from "next/font/google";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <NeynarContextProvider
        settings={{
          clientId: process.env.NEXT_PUBLIC_NEYNAR_CLIENT_ID || "",
          defaultTheme: Theme.Light,
          eventsCallbacks: {
            onAuthSuccess: () => {},
            onSignout() {},
          },
        }}
      >
        <body className={inter.className}>{children}</body>
      </NeynarContextProvider>
    </html>
  );
}

We are passing some settings here like clientId, defaultTheme and eventsCallbacks.

  • clientId: This is going to be the client ID you get from your neynar, add it to your .env.local file as NEXT_PUBLIC_NEYNAR_CLIENT_ID.

📘

Make sure to add localhost to the authorized origins

  • defaultTheme: default theme lets you change the theme of your sign-in button, currently, we have only light mode but dark mode is going to be live soon.
  • eventsCallbacks: This allows you to perform certain actions when the user signs out or auth is successful.

I've also added a styles import from the neynar react package here which is needed for the styles of the sign-in button.

Finally, let's add the sign-in button in the page.tsx file like this:

"use client";

import { NeynarAuthButton } from "@neynar/react";

export default function Login() {
  return (
    <div tw="flex items-center gap-4">
      <NeynarAuthButton />
    </div>
  );
}

If you head over to your app you'll be able to see a sign-in button on the screen. Go ahead and try signing in!

Now that our sign-in button is working let's add a call to add the users' signers to the database.

In layout.tsx add this to the onAuthSuccess function in NeynarContextProvider's settings:

eventsCallbacks: {
            onAuthSuccess: ({ user }) => {
              axios.post("/api/add-user", {
                signerUuid: user?.signer_uuid,
                fid: user?.fid,
              });
            },
            onSignout() {},
          },

This will call an /api/add-userAPI route which we are yet to create with the user's signer and fid every time a user successfully signs in.

Now, create a new /api/add-user/route.ts file in the app folder and add the following:

import { NextRequest, NextResponse } from "next/server";
import { isApiErrorResponse } from "@neynar/nodejs-sdk";
import neynarClient from "@/lib/neynarClient";
import prisma from "@/lib/prisma";

export async function POST(request: NextRequest) {
  const { signerUuid, fid } = (await request.json()) as {
    signerUuid: string;
    fid: string;
  };

  try {
    const { fid: userFid } = await neynarClient.lookupSigner(signerUuid);

    if (!userFid) {
      return NextResponse.json({ message: "User not found" }, { status: 404 });
    }

    if (fid !== String(userFid)) {
      return NextResponse.json(
        { message: "Invalid user data" },
        { status: 400 }
      );
    }

    const user = await prisma.user.findUnique({
      where: {
        fid: String(userFid),
      },
    });

    if (!user) {
      await prisma.user.create({
        data: {
          signerUUID: signerUuid,
          fid: String(userFid),
        },
      });
    }

    return NextResponse.json({ message: "User added" }, { status: 200 });
  } catch (err) {
    if (isApiErrorResponse(err)) {
      return NextResponse.json(
        { ...err.response.data },
        { status: err.response.status }
      );
    } else
      return NextResponse.json(
        { message: "Something went wrong" },
        { status: 500 }
      );
  }
}

Here we are verifying the signer is valid and if it's valid we add it to the database.

As you can see we are importing a neynarClient function which we have not yet created so we have to do that. Create a new lib/neynarClient.ts file and add the following:

import { NeynarAPIClient } from "@neynar/nodejs-sdk";

const neynarClient = new NeynarAPIClient(process.env.NEYNAR_API_KEY!);

export default neynarClient;

Creating the frame

Firstly, let's create the homescreen of the frame which will be a simple frame with a few buttons and a simple text.

So, create a new file frame/route.tsx file in the app folder and add the following:

import { createFrames, Button } from "frames.js/next";

const frames = createFrames({});

const HOST = process.env.HOST || "http://localhost:3000";

const handleRequest = frames(async () => {
  return {
    image: (
      <div tw="flex items-center justify-center h-full w-full bg-black">
        <p tw="text-white text-6xl flex">Cast from a frame!</p>
      </div>
    ),
    buttons: [
      <Button action="post" key="start" target={`${HOST}/frame/start`}>
        Start
      </Button>,
    ],
  };
});

export const GET = handleRequest;
export const POST = handleRequest;

If you open it up in a debugger it will show you a frame like this:


Now, create a new file start/route.tsx in the frame folder and add the following:

import prisma from "@/lib/prisma";
import { createFrames, Button } from "frames.js/next";

const frames = createFrames({});

const HOST = process.env.HOST || "http://localhost:3000";

const handleRequest = frames(async (payload) => {
  const fid = payload.message?.requesterFid;
  const user = await prisma.user.findUnique({
    where: {
      fid: String(fid),
    },
  });

  if (!user) {
    return {
      image: (
        <div tw="flex items-center justify-center h-full w-full bg-black">
          <p tw="text-white text-6xl">User not found!</p>
        </div>
      ),
      buttons: [
        <Button action="link" key="login" target={`${HOST}`}>
          Sign in
        </Button>,
      ],
    };
  }

  return {
    image: (
      <div tw="flex items-center justify-center h-full w-full bg-black">
        <p tw="text-white text-6xl">Cast from a frame!</p>
      </div>
    ),
    buttons: [
      <Button action="post" key="login" target={`${HOST}/frame/publish`}>
        Cast
      </Button>,
    ],
    textInput: "Text to cast...",
  };
});

export const GET = handleRequest;
export const POST = handleRequest;

This is just the home page for the frame that we are creating let's create a new /frame/publish route where the cast is made using the text that the user entered on the home screen.

Create a new file route.tsx in the frame/publish/ folder and add the following:

import neynarClient from "@/lib/neynarClient";
import prisma from "@/lib/prisma";
import { createFrames, Button } from "frames.js/next";

const frames = createFrames({});

const HOST = process.env.HOST || "http://localhost:3000";

const handleRequest = frames(async (payload) => {
  const text = payload.message?.inputText;
  const fid = payload.message?.requesterFid;

  const user = await prisma.user.findUnique({
    where: {
      fid: String(fid),
    },
  });

  if (!user) {
    return {
      image: (
        <div style={{ color: "white", display: "flex", fontSize: 60 }}>
          User not found!
        </div>
      ),
      buttons: [
        <Button action="link" key="login" target={HOST}>
          Sign in
        </Button>,
      ],
    };
  }

  const cast = await neynarClient.publishCast(user?.signerUUID, text || "gm");

  return {
    image: (
      <div tw="flex items-center justify-center h-full w-full bg-black">
        <div tw="text-white text-6xl flex">
          Casted successfully! 🎉
          {cast?.hash}
        </div>
      </div>
    ),
  };
});

export const GET = handleRequest;
export const POST = handleRequest;

Here, we are first confirming that the user's signer exists. And if it does we use it to create a new cast using the publishCast function from the client we initialised. If you try publishing a new cast on the frame it will create the cast and show you something like this.


Conclusion

This guide taught us how to create a frame from which the users can create casts, check out the GitHub repository if you want to look at the full code.

Lastly, make sure to share what you built with us on Farcaster by tagging @neynar and if you have any questions, reach out to us on warpcast or Telegram!