# How to Develop a Farcaster Frame for Moxie

*Build your first frame, start to finish*

By [48 Hours From Monday](https://paragraph.com/@48hrs) · 2024-08-30

---

Motivation
==========

Unlike most social apps, which hoard data and capitalize on the network effects brought about by their users, Farcaster enables developers to build unique experiences that leverage the network's social graph.

While having deep knowledge of full-stack web development or programming skills to build a Frame is unnecessary, a basic understanding will help. If you want to get an idea out there, there are [no-code solutions](https://neynar.com/nfs) for the less technically inclined.

For this tutorial, we're going to build a Frame for Moxie. A new social token protocol that lives on top of Farcaster, which rewards users for engagement and enables content creators to share earnings with their fans.

The frame we're building will enable users to see basic information about the fan tokens they hold.

**Note**: If you don't want to follow this tutorial but want to jump into the code, you can [clone my example repo](https://github.com/99darwin/moxie-fan-frame).

Prerequisites
=============

Before we start, you'll want to ensure you have the following prerequisites installed.

1.  NodeJS v20+ (I'm using 22.2.0)
    
2.  NPM v10+ (I'm using 10.7.0)
    
3.  [ngrok](https://dashboard.ngrok.com/get-started/setup/macos) (we'll use this for testing our Frame)
    
4.  A [Vercel](https://vercel.com) account (a free one is fine for our purposes)
    
5.  A [Neynar](https://neynar.com) account
    

Set up your environment
=======================

First, let's get a directory set up for our environment

    mkdir moxie-fans && cd moxie-fans

    npx create-next-app@latest ./

I recommend the following configuration

![](https://storage.googleapis.com/papyrus_images/71cd2c4de6e00f412fb7371f07bb8d80.png)

Now, install our dependencies.

    npm install --save dotenv frog hono graphql graphql-request ethers

**WARNING:** The next step is crucial for operation with Frog and NextJS.

Navigate to your `.eslintrc.json` file and add a rule to ignore `jsx`

    {
      "extends": "next/core-web-vitals",
      "rules": {
        "react/jsx-key": "off"
      }
    }

Set up file structure & configs
===============================

Next, we must set up files and configs to enable our Frame.

*   Navigate to `src/app/api` and make a new folder called `[[...routes]]`.
    
    *   Within `[[...routes]]` add a file called `route.tsx`
        
*   Add a folder called `gql` under the `api` folder.
    

*   Add a file under `gql` called `getUserOwnedFanTokens.ts` and add the following code
    
        import { gql } from "graphql-request";
        import { GraphQLClient } from "graphql-request";
        
        export const graphQLClient = new GraphQLClient(
         "https://api.studio.thegraph.com/query/23537/moxie_protocol_stats_mainnet/version/latest"
        );
        
        export const getUserOwnedFanTokens = async (address: string): Promise<any> => {
            const query = gql`
            query MyQuery($userAddresses: [ID!]) {
                users(where: { id_in: $userAddresses }) {
                  portfolio {
                    balance
                    buyVolume
                    sellVolume
                    subjectToken {
                      name
                      symbol
                    }
                  }
                }
              }
            `;
        
            const variable = {
                userAddresses: [address],
            };
        
            try {
                const data = await graphQLClient.request(query, variable);
                return data;
            } catch (e: any) {
                console.error("GraphQL request error:", e.message);
                return null;
            }
        }
    

*   The above code will enable us to pass in the frame user's address(es) and retrieve their portfolio. However, because most of the $MOXIE in the ecosystem is held in the vesting contract, we also need to get the relational address where the tokens are locked based on the beneficiary address we retrieve from the Frame.
    
*   Add another file under `gql` called `getVestingContractAddress.ts` and paste in the following code.
    
        import { gql } from "graphql-request";
        import { GraphQLClient } from "graphql-request";
        
        const graphQLClient = new GraphQLClient(
            "https://api.studio.thegraph.com/query/23537/moxie_vesting_mainnet/version/latest"
          );
        
        export const getVestingContractAddress = async (address: string): Promise<any> => {
            
            const query = gql`
                query MyQuery($beneficiary: Bytes) {
                    tokenLockWallets(where: {beneficiary: $beneficiary}) {
                        address: id
                    }
                }
            `;
        
            const variable = {
                beneficiary: address
            };
        
            try {
                const data = await graphQLClient.request(query, variable);
                return data;
              } catch (e: any) {
                console.error("GraphQL request error:", e.message);
                return null;
              }
        };
    
*   Create a new file in your top-level directory called `.env.local` and add the following variables (replacing YOURKEY with your API key).
    
        NEYNAR_API_URL="https://api.neynar.com/v2/farcaster"
        NEYNAR_API_KEY="YOURKEY"
    

**NOTE:** The following configuration is optional, but I recommend it for easily styling your Frames.

*   Add a new directory `src/app` called `utils` and place a file there called `ui.ts`. Add the following code.
    
        import { createSystem } from 'frog/ui'
         
        export const {
          Box,
          Columns,
          Column,
          Heading,
          HStack,
          Rows,
          Row,
          Spacer,
          Text,
          VStack,
          vars,
        } = createSystem()
    

Finally, let's get our ngrok server started to serve the Frames locally and test them in the [Frame Validator](https://warpcast.com/~/developers/frames).

    ngrok http http://localhost:3000

The above command will start up an ngrok server that will provide you with a URL you can use in the following steps. Keep this terminal window running and the URL handy.

Your file structure should look like this 👇

![](https://storage.googleapis.com/papyrus_images/4b5473104dd43d5c7b26f56a2128730a.png)

Create the Frame routes
=======================

Ok, we're finally ready to get creative and implement some interactivity!

Head back to `route.tsx` and import the necessary modules at the top of the file.

    /** @jsxImportSource frog/jsx */
    import { Button, Frog, TextInput } from 'frog';
    import { handle } from 'frog/next';
    import { neynar as neynarHub } from 'frog/hubs';
    import { neynar as neynarMiddleware } from 'frog/middlewares';
    import { ethers } from 'ethers';
    import { Box, Text, vars } from '@/app/utils/ui'; // Leave this out if you're not using frog ui
    import { getUserOwnedFanTokens } from './gql/getUserOwnedFanTokens';
    import { getVestingContractAddress } from '../gql/getVestingContractAddress';
    
    const INTENT_FOLLOW_ME = 'https://warpcast.com/nickysap'; // you can change this to your profile if you'd like

Next, let's instantiate Frog with the required configurations

    const { NEYNAR_API_KEY } = process.env;
    
    type State = {
        portfolio: any[];
    };
    
    const app = new Frog<{ State: State }>({
        title: 'Moxie Portfolio',
        ui: { vars },
        assetsPath: '/',
        basePath: '/api',
        imageAspectRatio: '1:1',
        imageOptions: {
            height: 1024,
            width: 1024,
        },
        initialState: {
            portfolio: [],
        },
        hub: neynarHub({ apiKey: NEYNAR_API_KEY }),
        verify: process.env.NODE_ENV === 'production',
        origin: BASE_URL,
        headers: { 'cache-control': 'max-age=0' },
    }).use(
        neynarMiddleware({
            apiKey: NEYNAR_API_KEY,
            features: ['interactor', 'cast'],
        })
    );

**IMPORTANT:** Place the following lines at the _bottom_ of your `routes.tsx` file.

    export const GET = handle(app);
    export const POST = handle(app);

Keep the above code below your routes throughout the tutorial.

Now, we will set up our routes.

The first route is just the landing Frame. We will keep it simple.

    app.frame('/', (c) => {
        return c.res({
            image: (
                <Box
                    alignVertical='center'
                    backgroundImage='url(https://moxie.xyz/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ffans.74a7f7cf.png&w=1200&q=75)'
                    backgroundColor='bg'
                    justifyContent='center'
                    paddingTop='52'
                    paddingLeft='80'
                    paddingRight='80'
                    backgroundSize='120% 150%'
                    backgroundPosition='top -10%'
                >
                    <Box
                        backgroundColor='linear'
                        borderTopLeftRadius='80'
                        borderTopRightRadius='80'
                        paddingTop='20'
                        paddingLeft='20'
                        paddingRight='20'
                        height='100%'
                        width='100%'
                        alignContent='center'
                        justifyContent='center'
                    >
                        <Text color='white' align='center' size='32'>
                            What&apos;s in your wallet?
                        </Text>
                        <Spacer size='24' />
                        <Text color='white' align='center' size='20'>
                            Check your Moxie Fan Token Portfolio
                        </Text>
                    </Box>
                </Box>
            ),
            intents: [
                <Button value='check'>Portfolio</Button>,
                <Button.Link href={INTENT_FOLLOW_ME}>Follow me</Button.Link>,
            ],
            action: '/portfolio',
        });
    });

The above code creates a frame with a background image and some text with two "intents." Intents are just actions the user can take on this Frame. In our case, we allow them to check their portfolio or follow my profile. Finally, the action tells the frame to send a request to the server and hit the `/portfolio` endpoint, which we will now create.

_You can go to the_ [_Frames Validator_](https://warpcast.com/~/developers/frames) _and check out your work if you'd like._

    app.frame('/portfolio', async (c) => {
        const { deriveState } = c;
        const state = await deriveState(async (previousState: any) => {
            const address = c.var.interactor?.verifications[0] as string;
            const vestingContractAddress = await getVestingContractAddress(address);
            const response = await getUserOwnedFanTokens(
                vestingContractAddress?.tokenLockWallets[0]?.address
            );
            previousState.portfolio = response?.users[0]?.portfolio || [];
            return previousState;
        });
    
        if (!state.portfolio || state.portfolio.length === 0) {
            return c.res({
                image: (
                    <Box
                        grow
                        backgroundColor='purple400'
                        display='flex'
                        alignItems='center'
                        justifyContent='center'
                    >
                        <Text>No Fan Tokens Found</Text>
                    </Box>
                ),
                intents: [
                    <Button value='back' action='/'>
                        Go back
                    </Button>,
                ],
            });
        }
    
        return renderPortfolio(c, state, 0);
    });

This code uses Frog's state management solution to derive and set the state of the user's portfolio. If there is nothing in the user's portfolio, we let them know and give them the option to return. If we find Fan Tokens in the portfolio, we use the `renderPortfolio` function.

Which reminds me, we need to write the `renderPortfolio` function!

Place the following code above your `/portfolio` endpoint.

    function renderPortfolio(c: any, state: any, page: number = 0) {
        const itemsPerPage = 5;
        const start = page * itemsPerPage;
        const end = start + itemsPerPage;
        const portfolioToShow = state.portfolio.slice(start, end);
        const portfolioLength = state.portfolio.length;
        const hasMore = portfolioLength > end;
        const address = c.var.interactor?.verifications[0] as string;
        return c.res({
            image: (
                <Box
                    alignVertical='center'
                    backgroundImage='url(https://moxie.xyz/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ffans.74a7f7cf.png&w=1200&q=75)'
                    backgroundColor='bg'
                    justifyContent='center'
                    paddingTop='30'
                    paddingLeft='80'
                    paddingRight='80'
                    backgroundSize='120% 150%'
                    backgroundPosition='top -10%'
                >
                    <Box
                        backgroundColor='linear'
                        borderTopLeftRadius='80'
                        borderTopRightRadius='80'
                        paddingTop='20'
                        paddingLeft='20'
                        paddingRight='20'
                        height='100%'
                        width='100%'
                        alignContent='center'
                        justifyContent='center'
                    >
                        <Text color='white' align='center' size='32'>
                            Your Moxie Fan Token Portfolio
                        </Text>
                        <Spacer size='24' />
                        {portfolioToShow.map((token: any) => (
                            <Box
                                display='flex'
                                flexDirection='row'
                                gap='4'
                                alignItems='flex-start'
                                justifyContent='center'
                            >
                                <Text color='purple400' size='20'>
                                    {token.subjectToken.name}:
                                </Text>
                                <Text color='white' size='20'>
                                    {parseInt(ethers.formatUnits(token.balance, 18))
                                        .toFixed(2)
                                        .toLocaleString()}
                                </Text>
                            </Box>
                        ))}
                        {hasMore && (
                            <Box
                                display='flex'
                                alignContent='center'
                                justifyContent='center'
                                paddingTop='10'
                            >
                                <Text color='white' align='center' size='16'>
                                    Plus {portfolioLength - end} more
                                </Text>
                                <Text color='white' align='center' size='16'>
                                    Click below to show more
                                </Text>
                            </Box>
                        )}
                    </Box>
                </Box>
            ),
            intents: [
                <Button action={`/stats/${address}`} value='stats'>
                    Stats
                </Button>,
                hasMore && (
                    <Button action={`/portfolio/${page + 1}`} value='more'>
                        More
                    </Button>
                ),
                page > 0 && (
                    <Button action='/portfolio' value='back'>
                        Back
                    </Button>
                ),
                !hasMore && (
                    <Button action={`/stats/${address}`} value='stats'>
                        Stats
                    </Button>
                ),
            ].filter(Boolean),
        });
    }

This function will enable us to paginate the user's portfolio, only showing five holdings per page to keep things visually appealing.

Now that we have this function, we can write the pagination endpoint.

    app.frame('/portfolio/:page', async (c) => {
        const page = parseInt(c.req.param('page') || '0');
        const { deriveState } = c;
        const state = await deriveState(async (previousState: any) => {
            if (!previousState.portfolio || previousState.portfolio.length === 0) {
                const address = c.var.interactor?.verifications[0] as string;
                const vestingContractAddress = await getVestingContractAddress(
                    address
                );
                const response = await getUserOwnedFanTokens(
                    vestingContractAddress?.tokenLockWallets[0]?.address
                );
                previousState.portfolio = response?.users[0]?.portfolio || [];
            }
            return previousState;
        });
    
        return renderPortfolio(c, state, page);
    });

The above endpoint requests the page number and displays the user's fan tokens based on the previous cursor.

Now, let's work on the `/stats` Frame will also have a share option so the user can share their stats as a Frame and entice others to check their stats and do the same.

    app.frame('/stats/:address', async (c) => {
        const address = c.req.param('address');
        const vestingContractAddress = await getVestingContractAddress(address);
        const response = await getUserOwnedFanTokens(
            vestingContractAddress?.tokenLockWallets[0]?.address
        );
        const totalTokens = response?.users[0].portfolio.length;
        const totalSupply = response?.users[0].portfolio.reduce(
            (acc: any, token: any) =>
                acc + parseInt(ethers.formatUnits(token.balance, 18)),
            0
        );
        const buyVolume = response?.users[0].portfolio.reduce(
            (acc: any, token: any) =>
                acc + parseInt(ethers.formatUnits(token.buyVolume, 18)),
            0
        );
        const sellVolume = response?.users[0].portfolio.reduce(
            (acc: any, token: any) =>
                acc + parseInt(ethers.formatUnits(token.sellVolume, 18)),
            0
        );
    
        const castUrl = 'https://warpcast.com/~/compose';
        const castText = 'I checked my Moxie Fan Token portfolio. Check yours out here!';
        const embedUrl = `${BASE_URL}/api/stats/${address}`;
        const INTENT_SHARE_FRAME = `${castUrl}?text=${encodeURIComponent(
            castText
        )}&embeds[]=${encodeURIComponent(embedUrl)}`;
    
        return c.res({
            image: (
                <Box
                    alignVertical='center'
                    backgroundImage='url(https://moxie.xyz/_next/image?url=%2F_next%2Fstatic%2Fmedia%2Ffans.74a7f7cf.png&w=1200&q=75)'
                    backgroundColor='bg'
                    justifyContent='center'
                    paddingTop='52'
                    paddingLeft='80'
                    paddingRight='80'
                    backgroundSize='120% 150%'
                    backgroundPosition='top -10%'
                >
                    <Box
                        backgroundColor='linear'
                        borderTopLeftRadius='80'
                        borderTopRightRadius='80'
                        paddingTop='20'
                        paddingLeft='20'
                        paddingRight='20'
                        height='100%'
                        width='100%'
                        alignContent='center'
                        justifyContent='center'
                    >
                        <Text color='white' align='center' size='32'>
                            Your Moxie Fan Token Stats
                        </Text>
                        <Spacer size='24' />
                        <Box
                            display='flex'
                            flexDirection='row'
                            gap='4'
                            alignItems='center'
                            justifyContent='center'
                        >
                            <Text color='purple400' size='20'>
                                Total Tokens:
                            </Text>
                            <Text color='white' size='20'>
                                {totalTokens}
                            </Text>
                        </Box>
                        <Box
                            display='flex'
                            flexDirection='row'
                            gap='4'
                            alignItems='center'
                            justifyContent='center'
                        >
                            <Text color='purple400' size='20'>
                                Total Supply:
                            </Text>
                            <Text color='white' size='20'>
                                {totalSupply.toFixed(2).toLocaleString()}
                            </Text>
                        </Box>
                        <Box
                            display='flex'
                            flexDirection='row'
                            gap='4'
                            alignItems='center'
                            justifyContent='center'
                        >
                            <Text color='purple400' size='20'>
                                Buy Volume ($MOXIE):
                            </Text>
                            <Text color='white' size='20'>
                                {buyVolume > 0
                                    ? buyVolume.toFixed(2).toLocaleString()
                                    : '0'}
                            </Text>
                        </Box>
                        <Box
                            display='flex'
                            flexDirection='row'
                            gap='4'
                            alignItems='center'
                            justifyContent='center'
                        >
                            <Text color='purple400' size='20'>
                                Sell Volume ($MOXIE):
                            </Text>
                            <Text color='white' size='20'>
                                {sellVolume > 0
                                    ? sellVolume.toFixed(2).toLocaleString()
                                    : '0'}
                            </Text>
                        </Box>
                    </Box>
                </Box>
            ),
            intents: [
                <Button action='/portfolio' value='back'>
                    Back
                </Button>,
                <Button.Link href={INTENT_SHARE_FRAME}>Share Frame</Button.Link>,
            ],
        });
    });

This Frame takes some of the additional information returned by our GraphQL query, formats it, and displays it at a glance for the user. This Frame could help check your total balance of Fan Tokens to help qualify your Far Boost score.

If the user selects 'Share Frame,' they will be led to a newly crafted cast with their user-specific stats already populated in-frame as an embed.

Now that we have a nice Frame with some useful interactivity and

Deploy
======

Set up your [git repository](https://github.com) and get the URL, then initialize git locally and get yourself all set up.

    git init
    git add .
    git commit -m "first commit"
    git branch -M main
    git remote add origin <YOUR-REPO-URL>
    git push -u origin main

Next, head over to [Vercel](https://vercel.com) and add a new project.

Import your newly created GitHub repository, and don't forget to add your environment variables!

Once deployed, you can head over to the [Frames Validator](https://warpcast.com/~/developers/frames) and paste in the URL Vercel provides you. Just remember to add `/api` at the end if you followed this tutorial.

Voila! You have just developed your first Moxie Frame!

---

*Originally published on [48 Hours From Monday](https://paragraph.com/@48hrs/how-to-develop-a-farcaster-frame-for-moxie)*
