# How to Develop a Farcaster Frame for Moxie > Build your first frame, start to finish **Published by:** [48 Hours From Monday](https://paragraph.com/@48hrs/) **Published on:** 2024-08-30 **URL:** https://paragraph.com/@48hrs/how-to-develop-a-farcaster-frame-for-moxie ## Content 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 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. Prerequisites Before we start, you'll want to ensure you have the following prerequisites installed. NodeJS v20+ (I'm using 22.2.0) NPM v10+ (I'm using 10.7.0) ngrok (we'll use this for testing our Frame) A Vercel account (a free one is fine for our purposes) A Neynar 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 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 => { 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 => { 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. 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 👇 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: ( What's in your wallet? Check your Moxie Fan Token Portfolio ), intents: [ , Follow me, ], 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 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: ( No Fan Tokens Found ), intents: [ , ], }); } 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: ( Your Moxie Fan Token Portfolio {portfolioToShow.map((token: any) => ( {token.subjectToken.name}: {parseInt(ethers.formatUnits(token.balance, 18)) .toFixed(2) .toLocaleString()} ))} {hasMore && ( Plus {portfolioLength - end} more Click below to show more )} ), intents: [ , hasMore && ( ), page > 0 && ( ), !hasMore && ( ), ].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: ( Your Moxie Fan Token Stats Total Tokens: {totalTokens} Total Supply: {totalSupply.toFixed(2).toLocaleString()} Buy Volume ($MOXIE): {buyVolume > 0 ? buyVolume.toFixed(2).toLocaleString() : '0'} Sell Volume ($MOXIE): {sellVolume > 0 ? sellVolume.toFixed(2).toLocaleString() : '0'} ), intents: [ , Share Frame, ], }); }); 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 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 git push -u origin main Next, head over to Vercel 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 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! ## Publication Information - [48 Hours From Monday](https://paragraph.com/@48hrs/): Publication homepage - [All Posts](https://paragraph.com/@48hrs/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@48hrs): Subscribe to updates - [Twitter](https://twitter.com/nicky_sap): Follow on Twitter ## Optional - [Collect as NFT](https://paragraph.com/@48hrs/how-to-develop-a-farcaster-frame-for-moxie): Support the author by collecting this post - [View Collectors](https://paragraph.com/@48hrs/how-to-develop-a-farcaster-frame-for-moxie/collectors): See who has collected this post