├── .gitignore ├── LICENSE ├── README.md ├── archiver-script ├── .gitignore ├── README.md ├── index.js ├── package-lock.json └── package.json ├── cast-action ├── .gitignore ├── README.md ├── bun.lockb ├── package.json ├── src │ ├── index.tsx │ └── lib │ │ └── neynarClient.ts └── tsconfig.json ├── flask-app ├── .env.example ├── .gitignore ├── README.md ├── app.py ├── requirements.txt └── templates │ └── index.html ├── frames-bot ├── .gitignore ├── README.md ├── bun.lockb ├── index.ts ├── package.json ├── tsconfig.json └── utils │ └── neynarClient.ts ├── funding.json ├── gm-bot ├── .env.example ├── .gitignore ├── README.md ├── ecosystem.config.cjs ├── getApprovedSigner.ts ├── package.json ├── src │ ├── abi │ │ ├── SignedKeyRequestMetadata.ts │ │ └── keyGateway.ts │ ├── app.ts │ ├── config.ts │ ├── neynarClient.ts │ ├── utils.ts │ └── viemClient.ts ├── tsconfig.json └── yarn.lock ├── managed-signers ├── .env.example ├── .gitignore ├── README.md ├── bun.lockb ├── next.config.mjs ├── package.json ├── src │ ├── app │ │ ├── api │ │ │ ├── cast │ │ │ │ └── route.ts │ │ │ ├── signer │ │ │ │ └── route.ts │ │ │ └── user │ │ │ │ └── route.ts │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.module.css │ │ └── page.tsx │ ├── constants.ts │ ├── lib │ │ └── neynarClient.ts │ └── utils │ │ ├── getFid.ts │ │ └── getSignedKey.ts └── tsconfig.json ├── wownar-react-native ├── .gitignore ├── README.md ├── client │ ├── .env.example │ ├── App.tsx │ ├── app.config.ts │ ├── app.json │ ├── assets │ │ ├── adaptive-icon.png │ │ ├── favicon.png │ │ ├── icon.png │ │ ├── splash.png │ │ ├── wownar.png │ │ └── wownar.svg │ ├── babel.config.js │ ├── constants.ts │ ├── package.json │ ├── src │ │ ├── AppNavigator.tsx │ │ ├── Context │ │ │ └── AppContext.tsx │ │ ├── components │ │ │ ├── Layout.tsx │ │ │ ├── Screens │ │ │ │ ├── Home.tsx │ │ │ │ ├── Loading.tsx │ │ │ │ └── Signin.tsx │ │ │ └── SignoutButton.tsx │ │ └── utils.ts │ ├── tsconfig.json │ └── yarn.lock └── server │ ├── .env.example │ ├── index.js │ ├── package.json │ └── yarn.lock ├── wownar-react-sdk ├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.js ├── package.json ├── postcss.config.js ├── public │ └── logos │ │ ├── wownar-black.svg │ │ └── wownar.svg ├── src │ ├── Context │ │ ├── AppContext.tsx │ │ └── NeynarProviderWrapper.tsx │ ├── app │ │ ├── Screens │ │ │ ├── Home │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── Signin │ │ │ │ └── index.tsx │ │ │ └── layout.tsx │ │ ├── api │ │ │ └── cast │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── globals.scss │ │ ├── layout.tsx │ │ └── page.tsx │ ├── clients │ │ └── neynar.ts │ └── components │ │ └── Button │ │ ├── index.module.scss │ │ └── index.tsx ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock └── wownar ├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.js ├── package.json ├── postcss.config.js ├── public └── logos │ ├── neynar.svg │ ├── powered-by-neynar.png │ ├── wownar-black.svg │ └── wownar.svg ├── src ├── Context │ └── AppContext.tsx ├── app │ ├── Screens │ │ ├── Home │ │ │ ├── index.module.scss │ │ │ └── index.tsx │ │ ├── Signin │ │ │ └── index.tsx │ │ └── layout.tsx │ ├── api │ │ ├── cast │ │ │ └── route.ts │ │ ├── user │ │ │ └── [fid] │ │ │ │ └── route.ts │ │ └── verify-user │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.scss │ ├── layout.tsx │ └── page.tsx ├── clients │ └── neynar.ts ├── components │ ├── Button │ │ ├── index.module.scss │ │ └── index.tsx │ └── icons │ │ └── Signout │ │ └── index.tsx ├── hooks │ └── use-local-storage-state.tsx ├── types.d.ts ├── utils │ └── helpers.ts └── window.d.ts ├── tailwind.config.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Neynar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # farcaster-examples 2 | A collection of Farcaster mini-apps powered by Neynar 3 | -------------------------------------------------------------------------------- /archiver-script/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /archiver-script/README.md: -------------------------------------------------------------------------------- 1 | # Neynar Archiver Mini-App 2 | 3 | Welcome to the Neynar Mini-App! This Nodejs script uses Neynar's API to fetch and archive casts of a specific user. Follow these steps to get started: 4 | 5 | ## Steps 6 | 7 | ### Step 1: Clone the Repository 8 | 9 | Clone the Neynar Mini-App repository to your local machine: 10 | 11 | ```sh 12 | git clone https://github.com/neynarxyz/farcaster-examples 13 | ``` 14 | 15 | ### Step 2: Prepare the Application Directory 16 | 17 | Move the Flask app directory to your desired location and remove the cloned repository's extra contents: 18 | 19 | ```sh 20 | mv farcaster-examples/archiver-script . 21 | rm -rf farcaster-examples 22 | cd archiver-script 23 | ``` 24 | 25 | ### Step 3: NPM install 26 | 27 | Install the required Nodejs packages: 28 | 29 | ```sh 30 | npm install 31 | ``` 32 | 33 | ### Step 4: Edit the FID and the API key 34 | 35 | Open `index.js` and replace the `FID` with your own: 36 | 37 | ```javascript 38 | // save all @rish.eth's casts in a file called data.ndjson 39 | const fid = 194; 40 | fetchAndDump(fid); 41 | ``` 42 | 43 | Also don't forget to edit use your API key: 44 | 45 | ```javascript 46 | const client = new NeynarAPIClient("YOUR_NEYNAR_API_KEY"); 47 | ``` 48 | 49 | ### Step 5: Run the Script 50 | 51 | ```sh 52 | node index.js 53 | ``` 54 | 55 | You should now be ready to run the script, saving the casts of the user with the FID you specified to a file called `data.ndjson` in the same directory as `index.js`. 56 | -------------------------------------------------------------------------------- /archiver-script/index.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | import { NeynarAPIClient } from "@neynar/nodejs-sdk"; 4 | const client = new NeynarAPIClient("YOUR_NEYNAR_API_KEY"); 5 | 6 | const parser = (cast) => { 7 | return { 8 | fid: parseInt(cast.author.fid), 9 | parentFid: parseInt(cast.parentAuthor.fid) 10 | ? parseInt(cast.parentAuthor.fid) 11 | : undefined, 12 | hash: cast.hash || undefined, 13 | threadHash: cast.threadHash || undefined, 14 | parentHash: cast.parentHash || undefined, 15 | parentUrl: cast.parentUrl || undefined, 16 | text: cast.text || undefined, 17 | }; 18 | }; 19 | 20 | // parse and save to file 21 | const dumpCast = (cast) => { 22 | const parsed = parser(cast); 23 | const data = `${JSON.stringify(parsed)}\n`; 24 | fs.appendFileSync("data.ndjson", data); 25 | }; 26 | 27 | const fetchAndDump = async (fid, cursor) => { 28 | const data = await client.fetchAllCastsCreatedByUser(fid, { 29 | limit: 150, 30 | cursor, 31 | }); 32 | data.result.casts.map(dumpCast); 33 | 34 | // If there is no next cursor, we are done 35 | if (data.result.next.cursor === null) return; 36 | await fetchAndDump(fid, data.result.next.cursor); 37 | }; 38 | 39 | // save all @rish.eth's casts in a file called data.ndjson 40 | const fid = 194; 41 | fetchAndDump(fid); 42 | -------------------------------------------------------------------------------- /archiver-script/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "archiver-script", 3 | "module": "index.js", 4 | "type": "module", 5 | "devDependencies": { 6 | "@biomejs/biome": "^1.4.1", 7 | "bun-types": "latest" 8 | }, 9 | "peerDependencies": { 10 | "typescript": "^5.0.0" 11 | }, 12 | "dependencies": { 13 | "@neynar/nodejs-sdk": "^1.1.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cast-action/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /cast-action/README.md: -------------------------------------------------------------------------------- 1 | # Cast Action 2 | 3 | In this guide, we’ll make a cast action with the neynar SDK and frog.fm, within a few minutes! The cast action will fetch the follower count of the cast's author using its fid and display it. 4 | 5 | Before we begin, you can access the [complete source code](https://github.com/neynarxyz/farcaster-examples/tree/main/cast-action) for this guide on GitHub. 6 | 7 | Let's get started! 8 | 9 |
10 | 11 | ## Creating a new frames project 12 | 13 | We will use [bun](https://bun.sh/) and [frog](https://frog.fm/) for building the cast action in this guide, but you can feel free to use anything else! 14 | 15 | Enter this command in your terminal to create a new app: 16 | 17 | ```powershell 18 | bunx create-frog -t bun 19 | ``` 20 | 21 | Enter a name for your project and it will spin up a new project for you. Once the project is created install the dependencies: 22 | 23 | ```powershell 24 | cd 25 | bun install 26 | ``` 27 | 28 | Now, let's install the dependencies that we are going to need to build out this action: 29 | 30 | ```powershell 31 | bun add @neynar/nodejs-sdk dotenv 32 | ``` 33 | 34 | ### Creating the cast action route 35 | 36 | Head over to the `src/index.ts` file. Here, you'll be able to see a starter frame on the / route. But first, let's change the Frog configuration to use `/api` as the base path and use neynar for hubs like this: 37 | 38 | ```typescript index.tsx 39 | export const app = new Frog({ 40 | hub: neynar({ apiKey: "NEYNAR_FROG_FM" }), 41 | basePath: "/api", 42 | }); 43 | ``` 44 | 45 | You also might need to import neynar from "frogs/neynar": 46 | 47 | ```typescript index.tsx 48 | import { neynar } from "frog/hubs"; 49 | ``` 50 | 51 | Now, we'll create a new post route which will handle our cast actions. So, create a new route like this: 52 | 53 | ```typescript index.tsx 54 | app.hono.post("/followers", async (c) => { 55 | try { 56 | let message = "GM"; 57 | return c.json({ message }); 58 | } catch (error) { 59 | console.error(error); 60 | } 61 | }); 62 | ``` 63 | 64 | This route will return a GM message every time the action is clicked, but let's now use the neynar SDK to get the follower count of the cast's author! 65 | 66 | Create a new `src/lib/neynarClient.ts` file and add the following: 67 | 68 | ```typescript neynarClient.ts 69 | import { NeynarAPIClient } from "@neynar/nodejs-sdk"; 70 | import { config } from "dotenv"; 71 | config(); 72 | 73 | if (!process.env.NEYNAR_API_KEY) { 74 | throw new Error("Make sure you set NEYNAR_API_KEY in your .env file"); 75 | } 76 | 77 | const neynarClient = new NeynarAPIClient(process.env.NEYNAR_API_KEY); 78 | 79 | export default neynarClient; 80 | ``` 81 | 82 | Here, we initialise the neynarClient with the neynar api key which you can get from your dashboard: 83 | 84 | ![](https://files.readme.io/794cfad-image.png) 85 | 86 | Add the api key in a `.env` file with the name `NEYNAR_API_KEY`. 87 | 88 | Head back to the `src/index.tsx` file and add the following in the followers route instead of the GM message: 89 | 90 | ```typescript index.tsx 91 | try { 92 | const body = await c.req.json(); 93 | const result = await neynarClient.validateFrameAction( 94 | body.trustedData.messageBytes 95 | ); 96 | 97 | const { users } = await neynarClient.fetchBulkUsers([ 98 | Number(result.action.cast.author.fid), 99 | ]); 100 | 101 | if (!users) { 102 | return c.json({ message: "Error. Try Again." }, 500); 103 | } 104 | 105 | let message = `Count:${users[0].follower_count}`; 106 | 107 | return c.json({ message }); 108 | } catch (e) { 109 | return c.json({ message: "Error. Try Again." }, 500); 110 | } 111 | ``` 112 | 113 | Here, we use the neynar client that we just initialised to first validate the action and get the data from the message bytes. Then, we use it to fetch the user information using the `fetchBulkUsers` function. Finally, we return a message with the follower count! 114 | 115 | ### Creating a frame with add cast action button 116 | 117 | I am also adding a simple frame that allows anyone to install the action. But for that, you need to host your server somewhere, for local development you can use ngrok. 118 | 119 | If you don’t already have it installed, install it from [here](https://ngrok.com/download). Once it’s installed authenticate using your auth token and serve your app using this command: 120 | 121 | ```powershell 122 | ngrok http http://localhost:5173/ 123 | ``` 124 | 125 | This command will give you a URL which will forward the requests to your localhost: 126 | 127 | ![](https://files.readme.io/9e1852c-image.png) 128 | 129 | You can now head over to the [cast action playground](https://warpcast.com/~/developers/cast-actions) and generate a new URL by adding in the info as such: 130 | 131 | ![](https://files.readme.io/47248c2-image.png) 132 | 133 | Copy the install URL and paste it into a new variable in the `index.tsx` like this: 134 | 135 | ```typescript index.tsx 136 | const ADD_URL = 137 | "https://warpcast.com/~/add-cast-action?actionType=post&name=Followers&icon=person&postUrl=https%3A%2F%2F05d3-2405-201-800c-6a-70a7-56e4-516c-2d3c.ngrok-free.app%2Fapi%2Ffollowers"; 138 | ``` 139 | 140 | Finally, you can replace the / route with the following to have a simple frame which links to this URL: 141 | 142 | ```typescript index.tsx 143 | app.frame("/", (c) => { 144 | return c.res({ 145 | image: ( 146 |
160 |

172 | gm! Add cast action to view followers count 173 |

174 |
175 | ), 176 | intents: [Add Action], 177 | }); 178 | }); 179 | ``` 180 | 181 | If you now start your server using `bun run dev` and head over to you'll be able to see a frame somewhat like this: 182 | 183 | ![](https://files.readme.io/ff041d7-image.png) 184 | 185 | Click on Add action and it'll prompt you to add a new action like this: 186 | 187 | ![](https://files.readme.io/08fc953-image.png) 188 | 189 | Once you have added the action, you can start using it on Warpcast to see the follower count of various people! 🥳 190 | 191 | ## Conclusion 192 | 193 | This guide taught us how to create a Farcaster cast action that shows the follower count of the cast's author! If you want to look at the completed code, check out the [GitHub repository](https://github.com/neynarxyz/farcaster-examples/tree/main/cast-action). 194 | 195 | Lastly, make sure to sure what you built with us on Farcaster by tagging [@neynar](https://warpcast.com/neynar) and if you have any questions, reach out to us on [warpcast](https://warpcast.com/~/channel/neynar) or [Telegram](https://t.me/rishdoteth)! 196 | -------------------------------------------------------------------------------- /cast-action/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katerinasmo/farcaster-examples/19a9506b4d10a580a50bcf5a136d0c76a1e96fd1/cast-action/bun.lockb -------------------------------------------------------------------------------- /cast-action/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cast-action-bun", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "frog dev", 7 | "serve": "bun run src/index.tsx", 8 | "build": "bun build src/index.tsx" 9 | }, 10 | "dependencies": { 11 | "@neynar/nodejs-sdk": "^1.16.0", 12 | "dotenv": "^16.4.5", 13 | "frog": "latest", 14 | "hono": "^4" 15 | }, 16 | "devDependencies": { 17 | "@types/bun": "latest", 18 | "bun": "latest" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cast-action/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Frog, TextInput } from "frog"; 2 | import { devtools } from "frog/dev"; 3 | import { serveStatic } from "frog/serve-static"; 4 | import neynarClient from "./lib/neynarClient.js"; 5 | import { neynar } from "frog/hubs"; 6 | 7 | export const app = new Frog({ 8 | hub: neynar({ apiKey: "NEYNAR_FROG_FM" }), 9 | basePath: "/api", 10 | }); 11 | 12 | const ADD_URL = 13 | "https://warpcast.com/~/add-cast-action?actionType=post&name=Followers&icon=person&postUrl=https%3A%2F%2F05d3-2405-201-800c-6a-70a7-56e4-516c-2d3c.ngrok-free.app%2Fapi%2Ffollowers"; 14 | 15 | app.frame("/", (c) => { 16 | return c.res({ 17 | image: ( 18 |
32 |

44 | gm! Add cast action to view followers count 45 |

46 |
47 | ), 48 | intents: [Add Action], 49 | }); 50 | }); 51 | 52 | app.hono.post("/followers", async (c) => { 53 | try { 54 | const body = await c.req.json(); 55 | const result = await neynarClient.validateFrameAction( 56 | body.trustedData.messageBytes 57 | ); 58 | 59 | const { users } = await neynarClient.fetchBulkUsers([ 60 | Number(result.action.cast.author.fid), 61 | ]); 62 | 63 | if (!users) { 64 | return c.json({ message: "Error. Try Again." }, 500); 65 | } 66 | 67 | let message = `Count:${users[0].follower_count}`; 68 | 69 | return c.json({ message }); 70 | } catch (e) { 71 | return c.json({ message: "Error. Try Again." }, 500); 72 | } 73 | }); 74 | 75 | app.use("/*", serveStatic({ root: "./public" })); 76 | devtools(app, { serveStatic }); 77 | 78 | if (typeof Bun !== "undefined") { 79 | Bun.serve({ 80 | fetch: app.fetch, 81 | port: 3000, 82 | }); 83 | console.log("Server is running on port 3000"); 84 | } 85 | -------------------------------------------------------------------------------- /cast-action/src/lib/neynarClient.ts: -------------------------------------------------------------------------------- 1 | import { NeynarAPIClient } from "@neynar/nodejs-sdk"; 2 | import { config } from "dotenv"; 3 | 4 | config(); 5 | 6 | if (!process.env.NEYNAR_API_KEY) { 7 | throw new Error("Make sure you set NEYNAR_API_KEY in your .env file"); 8 | } 9 | 10 | const neynarClient = new NeynarAPIClient(process.env.NEYNAR_API_KEY); 11 | 12 | export default neynarClient; 13 | -------------------------------------------------------------------------------- /cast-action/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "strict": true, 5 | "jsx": "react-jsx", 6 | "jsxImportSource": "hono/jsx" 7 | } 8 | } -------------------------------------------------------------------------------- /flask-app/.env.example: -------------------------------------------------------------------------------- 1 | NEYNAR_API_KEY=YOUR_API_KEY_HERE -------------------------------------------------------------------------------- /flask-app/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | *.py,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | db.sqlite3-journal 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | build/docs/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # pipenv 83 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 84 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 85 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 86 | # install all needed dependencies. 87 | #Pipfile.lock 88 | 89 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 90 | __pypackages__/ 91 | 92 | # Celery stuff 93 | celerybeat-schedule 94 | celerybeat.pid 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .envrc 102 | .venv 103 | venv/ 104 | ENV/ 105 | env/ 106 | env.bak/ 107 | env.tmp/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | 127 | # pytype static type analyzer 128 | .pytype/ 129 | 130 | # Cython debug symbols 131 | cython_debug/ 132 | -------------------------------------------------------------------------------- /flask-app/README.md: -------------------------------------------------------------------------------- 1 | # Neynar Flask Mini-App 2 | 3 | Welcome to the Neynar Mini-App! This Flask application uses Neynar's API to fetch and display casts from the EVM channel. Follow these steps to get started: 4 | 5 | ## Steps 6 | 7 | ### Step 1: Clone the Repository 8 | 9 | Clone the Neynar Mini-App repository to your local machine: 10 | 11 | ```sh 12 | git clone https://github.com/neynarxyz/farcaster-examples 13 | ``` 14 | 15 | ### Step 2: Prepare the Application Directory 16 | 17 | Move the Flask app directory to your desired location and remove the cloned repository's extra contents: 18 | 19 | ```sh 20 | mv farcaster-examples/flask-app . 21 | rm -rf farcaster-examples 22 | cd flask-app 23 | ``` 24 | 25 | ### Step 3: Set Up a Virtual Environment 26 | 27 | Create and activate a virtual environment for the application: 28 | 29 | ```sh 30 | python -m venv venv 31 | # Activate the environment: 32 | # On macOS/Linux: 33 | source venv/bin/activate 34 | # On Windows: 35 | .\venv\Scripts\activate 36 | ``` 37 | 38 | ### Step 4: Install Dependencies 39 | 40 | Install the required Python packages: 41 | 42 | ```sh 43 | pip install -r requirements.txt 44 | ``` 45 | 46 | ### Step 5: Edit the FID and the API key 47 | 48 | Open `app.py` and replace the `api_key` with your own: 49 | 50 | ```python 51 | api_key = "YOUR_API_KEY_HERE" 52 | ``` 53 | 54 | ### Step 6: Run the Application 55 | 56 | Start the Flask app: 57 | 58 | ```sh 59 | python app.py 60 | # Your app will be running on port 5000 61 | ``` 62 | 63 | You should now see the Neynar mini-app running and displaying casts from the EVM channel! 64 | -------------------------------------------------------------------------------- /flask-app/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | import requests 3 | import os 4 | 5 | # Initialize the Flask application 6 | app = Flask(__name__) 7 | 8 | 9 | @app.route("/") 10 | def main_app(): 11 | api_key = "YOUR_API_KEY_HERE" 12 | evm_channel = { 13 | "name": "EVM", 14 | "parent_url": "chain://eip155:1/erc721:0x37fb80ef28008704288087831464058a4a3940ae", 15 | "image": "https://warpcast.com/~/channel-images/evm.png", 16 | "channel_id": "evm", 17 | "lead_fid": 3621, 18 | } 19 | 20 | url = "https://api.neynar.com/v2/" 21 | url += "farcaster/feed?feed_type=filter&filter_type=parent_url" 22 | url += f"&parent_url={evm_channel['parent_url']}&limit=25" 23 | headers = {"accept": "application/json", "api_key": api_key} 24 | 25 | # get data from Neynar API, parse, then render template 26 | response = requests.get(url, headers=headers) 27 | data = response.json() 28 | 29 | # parse data 30 | parser = lambda cast: {"fid": cast["author"]["fid"], "text": cast["text"]} 31 | parsed_data = list(map(parser, data["casts"])) 32 | parsed_data = [cast for cast in parsed_data if cast["text"] not in ["", None]] 33 | 34 | return render_template("index.html", data=parsed_data) 35 | 36 | 37 | # Run the application 38 | if __name__ == "__main__": 39 | app.run(debug=True) 40 | -------------------------------------------------------------------------------- /flask-app/requirements.txt: -------------------------------------------------------------------------------- 1 | blinker==1.7.0 2 | certifi==2023.11.17 3 | charset-normalizer==3.3.2 4 | click==8.1.7 5 | Flask==3.0.0 6 | idna==3.6 7 | itsdangerous==2.1.2 8 | Jinja2==3.1.2 9 | MarkupSafe==2.1.3 10 | requests==2.31.0 11 | urllib3==2.1.0 12 | Werkzeug==3.0.1 13 | -------------------------------------------------------------------------------- /flask-app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | My app! 8 | 49 | 50 | 51 | 52 |
53 |

Casts in EVM channel!

54 | {% for item in data %} 55 |
56 |
fid:{{ item.fid }}
57 |
{{ item.text }}
58 |
59 | {% endfor %} 60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /frames-bot/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | -------------------------------------------------------------------------------- /frames-bot/README.md: -------------------------------------------------------------------------------- 1 | # Create a Farcaster bot to reply with frames using neynar 2 | 3 | In this guide, we’ll take a look at how to create a Farcaster bot that replies to specific keywords with a frame created on the go specifically for the reply! Here’s an example of the same: 4 | 5 | ![Demo of the farcaster frames bot](https://github.com/neynarxyz/farcaster-examples/assets/76690419/d5749625-ce9c-46da-b8aa-49bb9be8fd0f) 6 | 7 | For this guide, we'll go over: 8 | 9 | 1. Creating a webhook which listens to casts 10 | 2. Creating a bot which replies to the casts 11 | 3. Creating frames dynamically using the neynar SDK 12 | 13 | Before we begin, you can access the [complete source code](https://github.com/neynarxyz/farcaster-examples/tree/main/frames-bot) for this guide on GitHub. 14 | 15 | Let's get started! 16 | 17 | ## Setting up our server 18 | 19 | ### Creating a bun server 20 | 21 | I am going to use a [bun server](https://bun.sh/) for the sake of simplicity of this guide, but you can use express, Next.js api routes or any server that you wish to use! 22 | 23 | Create a new server by entering the following commands in your terminal: 24 | 25 | ```bash 26 | mkdir frames-bot 27 | cd frames-bot 28 | bun init 29 | ``` 30 | 31 | We are going to need the `@neynar/nodejs-sdk`, so let’s install that as well: 32 | 33 | ```tsx 34 | bun add @neynar/nodejs-sdk 35 | ``` 36 | 37 | Once the project is created and the packages are installed, you can open it in your favourite editor and add the following in `index.ts`: 38 | 39 | ```tsx 40 | const server = Bun.serve({ 41 | port: 3000, 42 | async fetch(req) { 43 | try { 44 | return new Response("Welcome to bun!"); 45 | } catch (e: any) { 46 | return new Response(e.message, { status: 500 }); 47 | } 48 | }, 49 | }); 50 | 51 | console.log(`Listening on localhost:${server.port}`); 52 | ``` 53 | 54 | This creates a server using bun which we will be using soon! 55 | 56 | Finally, run the server using the following command: 57 | 58 | ```bash 59 | bun run index.ts 60 | ``` 61 | 62 | ### Serve the app via ngrok 63 | 64 | We’ll serve the app using ngrok so, we can use this URL in the webhook. If you don’t already have it installed, install it from [here](https://ngrok.com/download). Once it’s installed authenticate using your auth token and serve your app using this command: 65 | 66 | ```bash 67 | ngrok http http://localhost:3000 68 | ``` 69 | 70 | ![Serve your app using ngrok](https://github.com/neynarxyz/farcaster-examples/assets/76690419/7f3e8d33-1aef-4723-948c-436c56658864) 71 | 72 | ## Creating a webhook 73 | 74 | We need to create a webhook on the neynar dashboard that will listen for certain words/mentions and call our server which will then reply to the cast. So, head over to the neynar dashboard and go to the [webhooks tab](https://dev.neynar.com/webhook). Click on new webhook and enter the details as such: 75 | 76 | ![Create a new webhook on the neynar dashboard](https://github.com/neynarxyz/farcaster-examples/assets/76690419/81b65ce0-5b3a-4856-b1e5-7f46c2c648cd) 77 | 78 | The target URL should be the URL you got from the ngrok command and you can select whichever event you want to listen to. I’ve chosen to listen to all the casts with “farcasterframesbot” in it. Once you have entered all the info click on create, and it will create a webhook for you. 79 | 80 | ## Creating the bot 81 | 82 | Head over to the [app section](https://dev.neynar.com/app) in the [neynar dashboard](https://dev.neynar.com/) and copy the signer uuid for your account: 83 | 84 | ![Copy the signer uuid for the bot](https://github.com/neynarxyz/farcaster-examples/assets/76690419/a6a56060-612c-4ff1-bf67-b01a0b43bcf3) 85 | 86 | Create a new `.env` file in the root of your project and add the following: 87 | 88 | ```bash 89 | SIGNER_UUID=your_signer_uuid 90 | NEYNAR_API_KEY=your_neynar_api_key 91 | ``` 92 | 93 | Add the signer UUID to the `SIGNER_UUID` and the neynar api key to the `NEYNAR_API_KEY` which you can get from the overview section of the neynar dashboard: 94 | 95 | ![Copy neynar api key from the dashboard](https://github.com/neynarxyz/farcaster-examples/assets/76690419/f55d7ee4-a2d2-4c61-ac0f-bf7074a80a60) 96 | 97 | Create a `neynarClient.ts` file and add the following: 98 | 99 | ```tsx 100 | import { NeynarAPIClient } from "@neynar/nodejs-sdk"; 101 | 102 | if (!process.env.NEYNAR_API_KEY) { 103 | throw new Error("Make sure you set NEYNAR_API_KEY in your .env file"); 104 | } 105 | 106 | const neynarClient = new NeynarAPIClient(process.env.NEYNAR_API_KEY); 107 | 108 | export default neynarClient; 109 | ``` 110 | 111 | Here we initialise the neynar client which we can use to publish casts. Head back to `index.ts` and add this inside the try block: 112 | 113 | ```tsx 114 | if (!process.env.SIGNER_UUID) { 115 | throw new Error("Make sure you set SIGNER_UUID in your .env file"); 116 | } 117 | 118 | const body = await req.text(); 119 | const hookData = JSON.parse(body); 120 | 121 | const reply = await neynarClient.publishCast( 122 | process.env.SIGNER_UUID, 123 | `gm ${hookData.data.author.username}`, 124 | { 125 | replyTo: hookData.data.hash, 126 | } 127 | ); 128 | console.log("reply:", reply); 129 | ``` 130 | 131 | You also need to import the neynar client in the `index.ts` file: 132 | 133 | ```tsx 134 | import neynarClient from "./neynarClient"; 135 | ``` 136 | 137 | This will now reply to every cast that has the word “farcasterframesbot” in it with a gm. Pretty cool, right? 138 | 139 | Let’s take this a step further and reply with a frame instead of boring texts! 140 | 141 | ## Creating the frame 142 | 143 | We’ll now generate a unique frame for every user on the fly using neynar frames. To create the frame add the following code in `index.ts` before the reply: 144 | 145 | ```tsx 146 | const creationRequest: NeynarFrameCreationRequest = { 147 | name: `gm ${hookData.data.author.username}`, 148 | pages: [ 149 | { 150 | image: { 151 | url: "https://moralis.io/wp-content/uploads/web3wiki/638-gm/637aeda23eca28502f6d3eae_61QOyzDqTfxekyfVuvH7dO5qeRpU50X-Hs46PiZFReI.jpeg", 152 | aspect_ratio: "1:1", 153 | }, 154 | title: "Page title", 155 | buttons: [], 156 | input: { 157 | text: { 158 | enabled: false, 159 | }, 160 | }, 161 | uuid: "gm", 162 | version: "vNext", 163 | }, 164 | ], 165 | }; 166 | 167 | const frame = await neynarClient.publishNeynarFrame(creationRequest); 168 | ``` 169 | 170 | You can edit the metadata here, I have just added a simple gm image but you can go crazy with it! Check out some templates in the [frame studio](https://dev.neynar.com/frames) for example. 171 | 172 | Anyways let’s continue building, you also need to add the frame as an embed in the reply body like this: 173 | 174 | ```tsx 175 | const reply = await neynarClient.publishCast( 176 | process.env.SIGNER_UUID, 177 | `gm ${hookData.data.author.username}`, 178 | { 179 | replyTo: hookData.data.hash, 180 | embeds: [ 181 | { 182 | url: frame.link, 183 | }, 184 | ], 185 | } 186 | ); 187 | ``` 188 | 189 | Putting it all together your final `index.ts` file should look similar to [this](https://github.com/neynarxyz/farcaster-examples/blob/main/frames-bot/index.ts). 190 | 191 | Don't forget to restart your server after making these changes! 192 | 193 | ```bash 194 | bun run index.ts 195 | ``` 196 | 197 | You can now create a cast on Farcaster and your webhook should be working just fine! 🥳 198 | 199 | ## Conclusion 200 | 201 | This guide taught us how to create a Farcaster bot that replies to specific keywords with a frame created on the go! If you want to look at the completed code, check out the [GitHub repository](https://github.com/neynarxyz/farcaster-examples/tree/main/frames-bot). 202 | 203 | Lastly, make sure to sure what you built with us on Farcaster by tagging [@neynar](https://warpcast.com/neynar) and if you have any questions, reach out to us on [warpcast](https://warpcast.com/~/channel/neynar) or [Telegram](https://t.me/rishdoteth)! 204 | -------------------------------------------------------------------------------- /frames-bot/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katerinasmo/farcaster-examples/19a9506b4d10a580a50bcf5a136d0c76a1e96fd1/frames-bot/bun.lockb -------------------------------------------------------------------------------- /frames-bot/index.ts: -------------------------------------------------------------------------------- 1 | import neynarClient from "./utils/neynarClient"; 2 | import { NeynarFrameCreationRequest } from "@neynar/nodejs-sdk/build/neynar-api/v2"; 3 | 4 | const server = Bun.serve({ 5 | port: 3000, 6 | async fetch(req) { 7 | try { 8 | const body = await req.text(); 9 | const hookData = JSON.parse(body); 10 | 11 | const creationRequest: NeynarFrameCreationRequest = { 12 | name: `gm ${hookData.data.author.username}`, 13 | pages: [ 14 | { 15 | image: { 16 | url: "https://moralis.io/wp-content/uploads/web3wiki/638-gm/637aeda23eca28502f6d3eae_61QOyzDqTfxekyfVuvH7dO5qeRpU50X-Hs46PiZFReI.jpeg", 17 | aspect_ratio: "1:1", 18 | }, 19 | title: "Page title", 20 | buttons: [], 21 | input: { 22 | text: { 23 | enabled: false, 24 | }, 25 | }, 26 | uuid: "gm", 27 | version: "vNext", 28 | }, 29 | ], 30 | }; 31 | const frame = await neynarClient.publishNeynarFrame(creationRequest); 32 | 33 | if (!process.env.SIGNER_UUID) { 34 | throw new Error("Make sure you set SIGNER_UUID in your .env file"); 35 | } 36 | 37 | const reply = await neynarClient.publishCast( 38 | process.env.SIGNER_UUID, 39 | `gm ${hookData.data.author.username}`, 40 | { 41 | embeds: [ 42 | { 43 | url: frame.link, 44 | }, 45 | ], 46 | replyTo: hookData.data.hash, 47 | } 48 | ); 49 | 50 | return new Response(`Replied to the cast with hash: ${reply.hash}`); 51 | } catch (e: any) { 52 | return new Response(e.message, { status: 500 }); 53 | } 54 | }, 55 | }); 56 | 57 | console.log(`Listening on localhost:${server.port}`); 58 | -------------------------------------------------------------------------------- /frames-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "farcaster-bot", 3 | "module": "index.ts", 4 | "type": "module", 5 | "scripts": { 6 | "start": "bun run index.ts" 7 | }, 8 | "devDependencies": { 9 | "bun-types": "latest" 10 | }, 11 | "peerDependencies": { 12 | "typescript": "^5.0.0" 13 | }, 14 | "dependencies": { 15 | "@neynar/nodejs-sdk": "^1.13.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frames-bot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": true, 14 | "jsx": "react-jsx", 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "allowJs": true, 18 | "types": [ 19 | "bun-types" // add Bun global 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frames-bot/utils/neynarClient.ts: -------------------------------------------------------------------------------- 1 | import { NeynarAPIClient } from "@neynar/nodejs-sdk"; 2 | 3 | if (!process.env.NEYNAR_API_KEY) { 4 | throw new Error("Make sure you set NEYNAR_API_KEY in your .env file"); 5 | } 6 | 7 | const neynarClient = new NeynarAPIClient(process.env.NEYNAR_API_KEY); 8 | 9 | export default neynarClient; 10 | -------------------------------------------------------------------------------- /funding.json: -------------------------------------------------------------------------------- 1 | { 2 | "opRetro": { 3 | "projectId": "0x6ad4cefe7290c830541e5477c95554c29d6b95f16e0942bc9d95472052da5802" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /gm-bot/.env.example: -------------------------------------------------------------------------------- 1 | PUBLISH_CAST_TIME # example 08:30 (24 hour time). If not provided will default to 09:00 2 | TIME_ZONE # example "America/New_York". If not provided will default to "UTC" 3 | NEYNAR_API_KEY="key" # Get API Key -> https://neynar.com/. 4 | FARCASTER_BOT_MNEMONIC="MNEMONIC" 5 | -------------------------------------------------------------------------------- /gm-bot/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.local 3 | node_modules 4 | dist 5 | pm2.log -------------------------------------------------------------------------------- /gm-bot/README.md: -------------------------------------------------------------------------------- 1 | # gm_bot 2 | 3 | ## Introduction 4 | 5 | `gm_bot` is an automated messaging bot designed to cast a 'gm 🪐' message in Warpcast every day at a scheduled time. The bot operates continuously as long as the system remains online. It leverages [Neynar API](https://docs.neynar.com/) and is built using [@neynar/nodejs-sdk](https://www.npmjs.com/package/@neynar/nodejs-sdk). 6 | 7 | ## Prerequisites 8 | 9 | - [Node.js](https://nodejs.org/en/): A JavaScript runtime built on Chrome's V8 JavaScript engine. Ensure you have Node.js installed on your system. 10 | 11 | ## Installation 12 | 13 | ### Setting Up the Environment 14 | 15 | 1. **Install PM2**: PM2 is a process manager for Node.js applications. Install it globally using npm: 16 | 17 | ```bash 18 | npm install -g pm2 19 | ``` 20 | 21 | 2. **Install Project Dependencies**: Navigate to the project directory and run one of the following commands to install all required dependencies: 22 | 23 | ```bash 24 | yarn install 25 | # or 26 | npm install 27 | ``` 28 | 29 | 3. **Configure Environment Variables**: 30 | - Copy the example environment file: 31 | ```bash 32 | cp .env.example .env 33 | ``` 34 | - Edit `.env` to add your `NEYNAR_API_KEY` and `FARCASTER_DEVELOPER_MNEMONIC`. Optionally, you can also specify `PUBLISH_CAST_TIME` and `TIME_ZONE` for custom scheduling. 35 | 36 | ### Generating a Signer 37 | 38 | Before running the bot, you need to generate a signer and get it approved via an onchain transaction. To execute the transaction, you'll need a browser extension wallet with funded roughly $2 worth of OP ETH on the Optimism mainnet. This is crucial for the bot's operation. Run the following command: 39 | 40 | ```bash 41 | yarn get-approved-signer 42 | ``` 43 | 44 | ### Approving a signer 45 | In order to get an approved signer you need to do an on-chain transaction on OP mainnet. 46 | Go to Farcaster KeyGateway optimism explorer 47 | https://optimistic.etherscan.io/address/0x00000000fc56947c7e7183f8ca4b62398caadf0b#writeContract 48 | 49 | Connect to Web3. 50 | 51 | Navigate to `addFor` function and add following values inside the respective placeholders. You will see values for fidOwner, keyType, key, metadataType, metadata, deadline, sig in your terminal logs. 52 | 53 | Press "Write" to execute the transaction. This will create a signer for your mnemonic on the OP mainnet. 54 | 55 | ## Running the Bot 56 | 57 | 1. **Start the Bot**: Launch the bot using the following command: 58 | 59 | ```bash 60 | yarn start 61 | # or 62 | npm run start 63 | ``` 64 | 65 | 2. **Verify the Process**: Ensure that the bot is running correctly with: 66 | 67 | ```bash 68 | pm2 status 69 | ``` 70 | 71 | 3. **View Logs**: To check the bot's activity logs, use: 72 | 73 | ```bash 74 | pm2 logs 75 | ``` 76 | 77 | 4. **Stopping the Bot**: If you need to stop the bot, use: 78 | ```bash 79 | pm2 kill 80 | ``` 81 | 82 | ## License 83 | 84 | `gm_bot` is released under the MIT License. This license permits free use, modification, and distribution of the software, with the requirement that the original copyright and license notice are included in any substantial portion of the work. 85 | 86 | ## FAQs/Troubleshooting 87 | 88 | - **Q1**: What if `gm_bot` stops sending messages? 89 | - **A1**: Check the PM2 logs for any errors and ensure your system's time settings align with the specified `TIME_ZONE`, also ensure that the process is running. 90 | -------------------------------------------------------------------------------- /gm-bot/ecosystem.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: "neynar-gm-bot", 5 | script: "./dist/app.js", 6 | instances: 1, 7 | exec_mode: "fork", 8 | autorestart: true, 9 | watch: false, 10 | max_memory_restart: "1G", 11 | env: { 12 | NODE_ENV: "production", 13 | }, 14 | }, 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /gm-bot/getApprovedSigner.ts: -------------------------------------------------------------------------------- 1 | import { getApprovedSigner } from "./src/utils"; 2 | getApprovedSigner(); -------------------------------------------------------------------------------- /gm-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gm_bot", 3 | "version": "1.0.0", 4 | "description": "A bot that will cast a 'gm 🪐' message in Warpcast at scheduled time everyday (As long as system is online)", 5 | "main": "./dist/app.js", 6 | "scripts": { 7 | "watch": "tsc --watch", 8 | "build": "rm -rf dist && tsc", 9 | "start": "npm run build && pm2 start ecosystem.config.cjs", 10 | "get-approved-signer": "ts-node getApprovedSigner.ts" 11 | }, 12 | "author": "Neynar", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@neynar/nodejs-sdk": "^0.11.3", 16 | "@types/node": "^20.9.0", 17 | "chatgpt": "^5.2.5", 18 | "dotenv": "^16.3.1", 19 | "node-cron": "^3.0.3", 20 | "typescript": "^5.2.2", 21 | "viem": "^1.19.0" 22 | }, 23 | "devDependencies": { 24 | "@types/node-cron": "^3.0.11", 25 | "ts-node": "^10.9.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /gm-bot/src/abi/SignedKeyRequestMetadata.ts: -------------------------------------------------------------------------------- 1 | export const SignedKeyRequestMetadataABI = { 2 | inputs: [ 3 | { 4 | components: [ 5 | { 6 | internalType: "uint256", 7 | name: "requestFid", 8 | type: "uint256", 9 | }, 10 | { 11 | internalType: "address", 12 | name: "requestSigner", 13 | type: "address", 14 | }, 15 | { 16 | internalType: "bytes", 17 | name: "signature", 18 | type: "bytes", 19 | }, 20 | { 21 | internalType: "uint256", 22 | name: "deadline", 23 | type: "uint256", 24 | }, 25 | ], 26 | internalType: "struct SignedKeyRequestValidator.SignedKeyRequestMetadata", 27 | name: "metadata", 28 | type: "tuple", 29 | }, 30 | ], 31 | name: "encodeMetadata", 32 | outputs: [ 33 | { 34 | internalType: "bytes", 35 | name: "", 36 | type: "bytes", 37 | }, 38 | ], 39 | stateMutability: "pure", 40 | type: "function", 41 | }; 42 | -------------------------------------------------------------------------------- /gm-bot/src/abi/keyGateway.ts: -------------------------------------------------------------------------------- 1 | export const keyGatewayAbi = [ 2 | { 3 | inputs: [ 4 | { internalType: "address", name: "_keyRegistry", type: "address" }, 5 | { internalType: "address", name: "_initialOwner", type: "address" }, 6 | ], 7 | stateMutability: "nonpayable", 8 | type: "constructor", 9 | }, 10 | { 11 | inputs: [ 12 | { internalType: "address", name: "account", type: "address" }, 13 | { internalType: "uint256", name: "currentNonce", type: "uint256" }, 14 | ], 15 | name: "InvalidAccountNonce", 16 | type: "error", 17 | }, 18 | { inputs: [], name: "InvalidShortString", type: "error" }, 19 | { inputs: [], name: "InvalidSignature", type: "error" }, 20 | { inputs: [], name: "OnlyGuardian", type: "error" }, 21 | { inputs: [], name: "SignatureExpired", type: "error" }, 22 | { 23 | inputs: [{ internalType: "string", name: "str", type: "string" }], 24 | name: "StringTooLong", 25 | type: "error", 26 | }, 27 | { 28 | anonymous: false, 29 | inputs: [ 30 | { 31 | indexed: true, 32 | internalType: "address", 33 | name: "guardian", 34 | type: "address", 35 | }, 36 | ], 37 | name: "Add", 38 | type: "event", 39 | }, 40 | { anonymous: false, inputs: [], name: "EIP712DomainChanged", type: "event" }, 41 | { 42 | anonymous: false, 43 | inputs: [ 44 | { 45 | indexed: true, 46 | internalType: "address", 47 | name: "previousOwner", 48 | type: "address", 49 | }, 50 | { 51 | indexed: true, 52 | internalType: "address", 53 | name: "newOwner", 54 | type: "address", 55 | }, 56 | ], 57 | name: "OwnershipTransferStarted", 58 | type: "event", 59 | }, 60 | { 61 | anonymous: false, 62 | inputs: [ 63 | { 64 | indexed: true, 65 | internalType: "address", 66 | name: "previousOwner", 67 | type: "address", 68 | }, 69 | { 70 | indexed: true, 71 | internalType: "address", 72 | name: "newOwner", 73 | type: "address", 74 | }, 75 | ], 76 | name: "OwnershipTransferred", 77 | type: "event", 78 | }, 79 | { 80 | anonymous: false, 81 | inputs: [ 82 | { 83 | indexed: false, 84 | internalType: "address", 85 | name: "account", 86 | type: "address", 87 | }, 88 | ], 89 | name: "Paused", 90 | type: "event", 91 | }, 92 | { 93 | anonymous: false, 94 | inputs: [ 95 | { 96 | indexed: true, 97 | internalType: "address", 98 | name: "guardian", 99 | type: "address", 100 | }, 101 | ], 102 | name: "Remove", 103 | type: "event", 104 | }, 105 | { 106 | anonymous: false, 107 | inputs: [ 108 | { 109 | indexed: false, 110 | internalType: "address", 111 | name: "account", 112 | type: "address", 113 | }, 114 | ], 115 | name: "Unpaused", 116 | type: "event", 117 | }, 118 | { 119 | inputs: [], 120 | name: "ADD_TYPEHASH", 121 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 122 | stateMutability: "view", 123 | type: "function", 124 | }, 125 | { 126 | inputs: [], 127 | name: "VERSION", 128 | outputs: [{ internalType: "string", name: "", type: "string" }], 129 | stateMutability: "view", 130 | type: "function", 131 | }, 132 | { 133 | inputs: [], 134 | name: "acceptOwnership", 135 | outputs: [], 136 | stateMutability: "nonpayable", 137 | type: "function", 138 | }, 139 | { 140 | inputs: [ 141 | { internalType: "uint32", name: "keyType", type: "uint32" }, 142 | { internalType: "bytes", name: "key", type: "bytes" }, 143 | { internalType: "uint8", name: "metadataType", type: "uint8" }, 144 | { internalType: "bytes", name: "metadata", type: "bytes" }, 145 | ], 146 | name: "add", 147 | outputs: [], 148 | stateMutability: "nonpayable", 149 | type: "function", 150 | }, 151 | { 152 | inputs: [ 153 | { internalType: "address", name: "fidOwner", type: "address" }, 154 | { internalType: "uint32", name: "keyType", type: "uint32" }, 155 | { internalType: "bytes", name: "key", type: "bytes" }, 156 | { internalType: "uint8", name: "metadataType", type: "uint8" }, 157 | { internalType: "bytes", name: "metadata", type: "bytes" }, 158 | { internalType: "uint256", name: "deadline", type: "uint256" }, 159 | { internalType: "bytes", name: "sig", type: "bytes" }, 160 | ], 161 | name: "addFor", 162 | outputs: [], 163 | stateMutability: "nonpayable", 164 | type: "function", 165 | }, 166 | { 167 | inputs: [{ internalType: "address", name: "guardian", type: "address" }], 168 | name: "addGuardian", 169 | outputs: [], 170 | stateMutability: "nonpayable", 171 | type: "function", 172 | }, 173 | { 174 | inputs: [], 175 | name: "domainSeparatorV4", 176 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 177 | stateMutability: "view", 178 | type: "function", 179 | }, 180 | { 181 | inputs: [], 182 | name: "eip712Domain", 183 | outputs: [ 184 | { internalType: "bytes1", name: "fields", type: "bytes1" }, 185 | { internalType: "string", name: "name", type: "string" }, 186 | { internalType: "string", name: "version", type: "string" }, 187 | { internalType: "uint256", name: "chainId", type: "uint256" }, 188 | { internalType: "address", name: "verifyingContract", type: "address" }, 189 | { internalType: "bytes32", name: "salt", type: "bytes32" }, 190 | { internalType: "uint256[]", name: "extensions", type: "uint256[]" }, 191 | ], 192 | stateMutability: "view", 193 | type: "function", 194 | }, 195 | { 196 | inputs: [{ internalType: "address", name: "guardian", type: "address" }], 197 | name: "guardians", 198 | outputs: [{ internalType: "bool", name: "isGuardian", type: "bool" }], 199 | stateMutability: "view", 200 | type: "function", 201 | }, 202 | { 203 | inputs: [{ internalType: "bytes32", name: "structHash", type: "bytes32" }], 204 | name: "hashTypedDataV4", 205 | outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], 206 | stateMutability: "view", 207 | type: "function", 208 | }, 209 | { 210 | inputs: [], 211 | name: "keyRegistry", 212 | outputs: [ 213 | { internalType: "contract IKeyRegistry", name: "", type: "address" }, 214 | ], 215 | stateMutability: "view", 216 | type: "function", 217 | }, 218 | { 219 | inputs: [{ internalType: "address", name: "owner", type: "address" }], 220 | name: "nonces", 221 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 222 | stateMutability: "view", 223 | type: "function", 224 | }, 225 | { 226 | inputs: [], 227 | name: "owner", 228 | outputs: [{ internalType: "address", name: "", type: "address" }], 229 | stateMutability: "view", 230 | type: "function", 231 | }, 232 | { 233 | inputs: [], 234 | name: "pause", 235 | outputs: [], 236 | stateMutability: "nonpayable", 237 | type: "function", 238 | }, 239 | { 240 | inputs: [], 241 | name: "paused", 242 | outputs: [{ internalType: "bool", name: "", type: "bool" }], 243 | stateMutability: "view", 244 | type: "function", 245 | }, 246 | { 247 | inputs: [], 248 | name: "pendingOwner", 249 | outputs: [{ internalType: "address", name: "", type: "address" }], 250 | stateMutability: "view", 251 | type: "function", 252 | }, 253 | { 254 | inputs: [{ internalType: "address", name: "guardian", type: "address" }], 255 | name: "removeGuardian", 256 | outputs: [], 257 | stateMutability: "nonpayable", 258 | type: "function", 259 | }, 260 | { 261 | inputs: [], 262 | name: "renounceOwnership", 263 | outputs: [], 264 | stateMutability: "nonpayable", 265 | type: "function", 266 | }, 267 | { 268 | inputs: [{ internalType: "address", name: "newOwner", type: "address" }], 269 | name: "transferOwnership", 270 | outputs: [], 271 | stateMutability: "nonpayable", 272 | type: "function", 273 | }, 274 | { 275 | inputs: [], 276 | name: "unpause", 277 | outputs: [], 278 | stateMutability: "nonpayable", 279 | type: "function", 280 | }, 281 | { 282 | inputs: [], 283 | name: "useNonce", 284 | outputs: [{ internalType: "uint256", name: "", type: "uint256" }], 285 | stateMutability: "nonpayable", 286 | type: "function", 287 | }, 288 | ] as const; 289 | -------------------------------------------------------------------------------- /gm-bot/src/app.ts: -------------------------------------------------------------------------------- 1 | import cron from "node-cron"; 2 | import { MESSAGE } from "./utils"; 3 | import neynarClient from "./neynarClient"; 4 | import { 5 | PUBLISH_CAST_TIME, 6 | SIGNER_UUID, 7 | TIME_ZONE, 8 | NEYNAR_API_KEY, 9 | } from "./config"; 10 | import { isApiErrorResponse } from "@neynar/nodejs-sdk"; 11 | 12 | // Validating necessary environment variables or configurations. 13 | if (!SIGNER_UUID) { 14 | throw new Error("SIGNER_UUID is not defined"); 15 | } 16 | 17 | if (!NEYNAR_API_KEY) { 18 | throw new Error("NEYNAR_API_KEY is not defined"); 19 | } 20 | 21 | /** 22 | * Function to publish a message (cast) using neynarClient. 23 | * @param msg - The message to be published. 24 | */ 25 | const publishCast = async (msg: string) => { 26 | try { 27 | // Using the neynarClient to publish the cast. 28 | await neynarClient.publishCast(SIGNER_UUID, msg); 29 | console.log("Cast published successfully"); 30 | } catch (err) { 31 | // Error handling, checking if it's an API response error. 32 | if (isApiErrorResponse(err)) { 33 | console.log(err.response.data); 34 | } else console.log(err); 35 | } 36 | }; 37 | 38 | // Initial call to publish a motivational message. 39 | publishCast( 40 | `gm! I'm here to brighten your day with daily cheer. Look forward to a warm 'gm' everyday!` 41 | ); 42 | 43 | // Extracting hour and minute from the PUBLISH_CAST_TIME configuration. 44 | const [hour, minute] = PUBLISH_CAST_TIME.split(":"); 45 | 46 | // Scheduling a cron job to publish a message at a specific time every day. 47 | cron.schedule( 48 | `${minute} ${hour} * * *`, // Cron time format 49 | function () { 50 | publishCast(MESSAGE); // Function to execute at the scheduled time. 51 | }, 52 | { 53 | scheduled: true, // Ensure the job is scheduled. 54 | timezone: TIME_ZONE, // Set the timezone for the schedule. 55 | } 56 | ); 57 | 58 | // Logging to inform that the cron job is scheduled. 59 | console.log( 60 | `Cron job scheduled at ${PUBLISH_CAST_TIME} ${TIME_ZONE}, please don't restart your system before the scheduled time.` 61 | ); 62 | -------------------------------------------------------------------------------- /gm-bot/src/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config(); 3 | 4 | export const FARCASTER_BOT_MNEMONIC = process.env.FARCASTER_BOT_MNEMONIC!; 5 | export const SIGNER_UUID = process.env.SIGNER_UUID!; 6 | export const NEYNAR_API_KEY = process.env.NEYNAR_API_KEY!; 7 | export const PUBLISH_CAST_TIME = process.env.PUBLISH_CAST_TIME || "09:00"; 8 | export const TIME_ZONE = process.env.TIME_ZONE || "UTC"; 9 | -------------------------------------------------------------------------------- /gm-bot/src/neynarClient.ts: -------------------------------------------------------------------------------- 1 | import { NeynarAPIClient } from "@neynar/nodejs-sdk"; 2 | import { NEYNAR_API_KEY } from "./config"; 3 | 4 | const neynarClient = new NeynarAPIClient(NEYNAR_API_KEY); 5 | 6 | export default neynarClient; 7 | -------------------------------------------------------------------------------- /gm-bot/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { FARCASTER_BOT_MNEMONIC } from "./config"; 2 | import neynarClient from "./neynarClient"; 3 | import { mnemonicToAccount } from "viem/accounts"; 4 | import { viemPublicClient } from "./viemClient"; 5 | import { keyGatewayAbi } from "./abi/keyGateway"; 6 | import { encodeAbiParameters } from "viem"; 7 | import { SignedKeyRequestMetadataABI } from "./abi/SignedKeyRequestMetadata"; 8 | import { SignerStatusEnum } from "@neynar/nodejs-sdk/build/neynar-api/neynar-v2-api"; 9 | import * as fs from "fs"; 10 | import * as path from "path"; 11 | import { isApiErrorResponse } from "@neynar/nodejs-sdk"; 12 | 13 | // A constant message for greeting or logging. 14 | export const MESSAGE = `gm 🪐`; 15 | 16 | /** 17 | * Appends the signer_uuid to the .env file. 18 | * @param signer_uuid - Approved signer UUID of the user. 19 | */ 20 | const appendSignerUuidAndUsernameToEnv = (signer_uuid: string) => { 21 | // Resolving the path to the .env file. 22 | const envPath = path.resolve(__dirname, "../.env"); 23 | 24 | // Reading the .env file. 25 | fs.readFile(envPath, "utf8", (err, data) => { 26 | if (err) { 27 | console.error("Error reading .env file:", err); 28 | return; 29 | } 30 | 31 | // Appending the SIGNER_UUID to the file content. 32 | const newContent = data + `\nSIGNER_UUID=${signer_uuid}`; 33 | 34 | // Writing the updated content back to the .env file. 35 | fs.writeFile(envPath, newContent, "utf8", (err) => { 36 | if (err) { 37 | console.error("Error writing to .env file:", err); 38 | return; 39 | } 40 | console.log( 41 | "SIGNER_UUID appended to .env file.\nPlease run `yarn start` to continue.\n" 42 | ); 43 | }); 44 | }); 45 | }; 46 | 47 | /** 48 | * Generates an approved signer for the user. 49 | */ 50 | export const getApprovedSigner = async () => { 51 | try { 52 | // Creating a new signer and obtaining its public key and UUID. 53 | const { public_key: signerPublicKey, signer_uuid } = 54 | await neynarClient.createSigner(); 55 | 56 | // Constants for the EIP-712 domain and request type, required for signing data. 57 | // DO NOT CHANGE ANY VALUES IN THESE CONSTANTS 58 | const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = { 59 | name: "Farcaster SignedKeyRequestValidator", // EIP-712 domain data for the SignedKeyRequestValidator. 60 | version: "1", 61 | chainId: 10, 62 | verifyingContract: 63 | "0x00000000fc700472606ed4fa22623acf62c60553" as `0x${string}`, 64 | }; 65 | 66 | // DO NOT CHANGE ANY VALUES IN THIS CONSTANT 67 | const SIGNED_KEY_REQUEST_TYPE = [ 68 | { name: "requestFid", type: "uint256" }, 69 | { name: "key", type: "bytes" }, 70 | { name: "deadline", type: "uint256" }, 71 | ]; 72 | 73 | // Convert mnemonic to an account object. 74 | const account = mnemonicToAccount(FARCASTER_BOT_MNEMONIC); 75 | 76 | // Lookup user details using the custody address. 77 | const { user: farcasterDeveloper } = 78 | await neynarClient.lookupUserByCustodyAddress(account.address); 79 | 80 | console.log( 81 | `✅ Detected user with fid ${farcasterDeveloper.fid} and custody address: ${farcasterDeveloper.custody_address}` 82 | ); 83 | 84 | // Generates an expiration date for the signature 85 | // e.g. 1693927665 86 | const deadline = Math.floor(Date.now() / 1000) + 86400; // signature is valid for 1 day from now 87 | 88 | // Signing the key request data. 89 | let signature = await account.signTypedData({ 90 | domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN, 91 | types: { 92 | SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE, 93 | }, 94 | primaryType: "SignedKeyRequest", 95 | message: { 96 | requestFid: BigInt(farcasterDeveloper.fid), 97 | key: signerPublicKey, 98 | deadline: BigInt(deadline), 99 | }, 100 | }); 101 | 102 | // Encoding ABI parameters for the metadata. 103 | const metadata = encodeAbiParameters(SignedKeyRequestMetadataABI.inputs, [ 104 | { 105 | requestFid: BigInt(farcasterDeveloper.fid), 106 | requestSigner: account.address, 107 | signature: signature, 108 | deadline: BigInt(deadline), 109 | }, 110 | ]); 111 | 112 | // Interacting with a blockchain contract to get a nonce value. 113 | const developerKeyGatewayNonce = await viemPublicClient.readContract({ 114 | address: "0x00000000fc56947c7e7183f8ca4b62398caadf0b", // gateway address 115 | abi: keyGatewayAbi, 116 | functionName: "nonces", 117 | args: [farcasterDeveloper.custody_address as `0x${string}`], 118 | }); 119 | 120 | // Additional EIP-712 domain and type definitions for the key gateway. 121 | const KEY_GATEWAY_EIP_712_DOMAIN = { 122 | name: "Farcaster KeyGateway", 123 | version: "1", 124 | chainId: 10, 125 | verifyingContract: 126 | "0x00000000fc56947c7e7183f8ca4b62398caadf0b" as `0x${string}`, 127 | }; 128 | 129 | // Signing data for the Add operation. 130 | const ADD_TYPE = [ 131 | { name: "owner", type: "address" }, 132 | { name: "keyType", type: "uint32" }, 133 | { name: "key", type: "bytes" }, 134 | { name: "metadataType", type: "uint8" }, 135 | { name: "metadata", type: "bytes" }, 136 | { name: "nonce", type: "uint256" }, 137 | { name: "deadline", type: "uint256" }, 138 | ]; 139 | 140 | signature = await account.signTypedData({ 141 | domain: KEY_GATEWAY_EIP_712_DOMAIN, 142 | types: { 143 | Add: ADD_TYPE, 144 | }, 145 | primaryType: "Add", 146 | message: { 147 | owner: account.address, 148 | keyType: 1, 149 | key: signerPublicKey, 150 | metadataType: 1, 151 | metadata: metadata, 152 | nonce: BigInt(developerKeyGatewayNonce), 153 | deadline: BigInt(deadline), 154 | }, 155 | }); 156 | 157 | // Logging instructions and values for the user to perform on-chain transactions. 158 | console.log("✅ Generated signer", "\n"); 159 | 160 | console.log( 161 | "In order to get an approved signer you need to do an on-chain transaction on OP mainnet. \nGo to Farcaster KeyGateway optimism explorer\nhttps://optimistic.etherscan.io/address/0x00000000fc56947c7e7183f8ca4b62398caadf0b#writeContract \n" 162 | ); 163 | console.log( 164 | "Connect to Web3.\n\nNavigate to `addFor` function and add following values inside the respective placeholders.\n" 165 | ); 166 | 167 | console.log( 168 | "fidOwner (address) :=> ", 169 | farcasterDeveloper.custody_address, 170 | "\n -" 171 | ); 172 | console.log("keyType (uint32) :=> ", 1, "\n -"); 173 | console.log("key (bytes) :=> ", signerPublicKey, "\n -"); 174 | console.log("metadataType (uint8) :=> ", 1, "\n -"); 175 | console.log("metadata (bytes) :=> ", metadata, "\n -"); 176 | console.log("deadline (uint256) :=> ", deadline, "\n -"); 177 | console.log("sig (bytes) :=> ", signature, "\n -\n"); 178 | console.log( 179 | "We are polling for the signer to be approved. It will be approved once the onchain transaction is confirmed." 180 | ); 181 | console.log("Checking for the status of signer..."); 182 | 183 | // Polling for the signer status until it is approved. 184 | while (true) { 185 | const res = await neynarClient.lookupSigner(signer_uuid); 186 | if (res && res.status === SignerStatusEnum.Approved) { 187 | console.log("✅ Approved signer", signer_uuid); 188 | break; 189 | } 190 | console.log("Waiting for signer to be approved..."); 191 | await new Promise((r) => setTimeout(r, 5000)); 192 | } 193 | 194 | console.log("✅ Transaction confirmed\n"); 195 | console.log("✅ Approved signer", signer_uuid, "\n"); 196 | // Once approved, appending the signer UUID to the .env file. 197 | appendSignerUuidAndUsernameToEnv(signer_uuid); 198 | } catch (err) { 199 | // Error handling, checking if it's an API response error. 200 | if (isApiErrorResponse(err)) { 201 | console.log(err.response.data); 202 | } else console.log(err); 203 | } 204 | }; 205 | -------------------------------------------------------------------------------- /gm-bot/src/viemClient.ts: -------------------------------------------------------------------------------- 1 | import { createPublicClient, http } from "viem"; 2 | import { optimism } from "viem/chains"; 3 | 4 | export const viemPublicClient = createPublicClient({ 5 | chain: optimism, 6 | transport: http(), 7 | }); 8 | -------------------------------------------------------------------------------- /gm-bot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "allowJs": true, 5 | "checkJs": false, 6 | "outDir": "dist", 7 | "sourceMap": true, 8 | "strict": true, 9 | "alwaysStrict": true, 10 | "target": "es5", 11 | "module": "CommonJS", 12 | "esModuleInterop": true, 13 | "moduleResolution": "node" 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | -------------------------------------------------------------------------------- /managed-signers/.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | NEYNAR_API_KEY= 3 | FARCASTER_DEVELOPER_MNEMONIC= 4 | -------------------------------------------------------------------------------- /managed-signers/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /managed-signers/README.md: -------------------------------------------------------------------------------- 1 | # Write casts with managed signers 2 | 3 | ## Prerequisites 4 | 5 | - Make sure you have Node.js and yarn installed on your machine. 6 | 7 | ## Setup 8 | 9 | 1. **Install Dependencies**: 10 | To ensure that all the required libraries and modules are installed, run: 11 | 12 | ```bash 13 | yarn install 14 | ``` 15 | 16 | 2. **Environment Configuration**: 17 | We use environment variables for various configurations. Start by copying the example environment variables file: 18 | 19 | ```bash 20 | cp .env.example .env.local 21 | ``` 22 | 23 | 3. **Update Environment Variables**: 24 | Open the `.env.local` file in your favorite editor and make sure to replace placeholders with the correct values. 25 | 26 | ```bash 27 | open .env.local 28 | ``` 29 | 30 | 🔔 Notes: 31 | 32 | - NEYNAR_API_KEY: If you need one, sign up on [https://neynar.com](https://neynar.com) 33 | - FARCASTER_DEVELOPER_MNEMONIC: The 12 or 24 word recovery phrase for the Farcaster account. Make sure the value is within single quotes. 34 | - A farcaster developer account is the same as any farcaster account. You can use your personal account as a farcaster developer account e.g. https://warpcast.com/manan or your company / branded developer account like https://warpcast.com/neynar 35 | 36 | 4. **Start the App**: 37 | To start the Sample Farcaster app in development mode, run: 38 | 39 | ```bash 40 | yarn start 41 | ``` 42 | 43 | Your app should now be running on `localhost:3000` (or a specified port in your `.env`). 44 | 45 | ## Troubleshooting 46 | 47 | - If you run into issues with missing packages, make sure to run `yarn install` again. 48 | - Ensure all environment variables in `.env.local` are correctly set. 49 | 50 | ## Feedback 51 | 52 | If you have any feedback or run into issues, please reach out to our team or create an issue on the repository. 53 | 54 | --- 55 | 56 | Happy coding! 🚀 57 | -------------------------------------------------------------------------------- /managed-signers/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/katerinasmo/farcaster-examples/19a9506b4d10a580a50bcf5a136d0c76a1e96fd1/managed-signers/bun.lockb -------------------------------------------------------------------------------- /managed-signers/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | images: { 5 | remotePatterns: [ 6 | { 7 | hostname: "*", 8 | protocol: "http", 9 | }, 10 | { 11 | hostname: "*", 12 | protocol: "https", 13 | }, 14 | ], 15 | }, 16 | }; 17 | 18 | export default nextConfig; 19 | -------------------------------------------------------------------------------- /managed-signers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "managed-signers", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@farcaster/hub-nodejs": "^0.11.8", 13 | "@neynar/nodejs-sdk": "^1.16.0", 14 | "axios": "^1.6.8", 15 | "next": "14.1.4", 16 | "qrcode.react": "^3.1.0", 17 | "react": "^18", 18 | "react-dom": "^18", 19 | "viem": "^2.9.15" 20 | }, 21 | "devDependencies": { 22 | "typescript": "^5", 23 | "@types/node": "^20", 24 | "@types/react": "^18", 25 | "@types/react-dom": "^18" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /managed-signers/src/app/api/cast/route.ts: -------------------------------------------------------------------------------- 1 | import neynarClient from "@/lib/neynarClient"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function POST(req: Request) { 5 | const body = await req.json(); 6 | 7 | try { 8 | const cast = await neynarClient.publishCast(body.signer_uuid, body.text); 9 | 10 | return NextResponse.json(cast, { status: 200 }); 11 | } catch (error) { 12 | console.error(error); 13 | return NextResponse.json({ error: "An error occurred" }, { status: 500 }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /managed-signers/src/app/api/signer/route.ts: -------------------------------------------------------------------------------- 1 | import neynarClient from "@/lib/neynarClient"; 2 | import { getSignedKey } from "@/utils/getSignedKey"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function POST() { 6 | try { 7 | const signedKey = await getSignedKey(); 8 | 9 | return NextResponse.json(signedKey, { 10 | status: 200, 11 | }); 12 | } catch (error) { 13 | return NextResponse.json({ error: "An error occurred" }, { status: 500 }); 14 | } 15 | } 16 | 17 | export async function GET(req: Request) { 18 | const { searchParams } = new URL(req.url); 19 | const signer_uuid = searchParams.get("signer_uuid"); 20 | 21 | if (!signer_uuid) { 22 | return NextResponse.json( 23 | { error: "signer_uuid is required" }, 24 | { status: 400 } 25 | ); 26 | } 27 | 28 | try { 29 | const signer = await neynarClient.lookupSigner(signer_uuid); 30 | 31 | return NextResponse.json(signer, { status: 200 }); 32 | } catch (error) { 33 | return NextResponse.json({ error: "An error occurred" }, { status: 500 }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /managed-signers/src/app/api/user/route.ts: -------------------------------------------------------------------------------- 1 | import neynarClient from "@/lib/neynarClient"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET(req: Request) { 5 | const { searchParams } = new URL(req.url); 6 | const fid = searchParams.get("fid"); 7 | 8 | if (!fid) { 9 | return NextResponse.json({ error: "fid is required" }, { status: 400 }); 10 | } 11 | 12 | try { 13 | const user = await neynarClient.fetchBulkUsers([Number(fid)]); 14 | 15 | return NextResponse.json(user.users[0], { status: 200 }); 16 | } catch (error) { 17 | return NextResponse.json({ error: "An error occurred" }, { status: 500 }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /managed-signers/src/app/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | html, 8 | body { 9 | max-width: 100vw; 10 | overflow-x: hidden; 11 | } 12 | 13 | body { 14 | color: #fff; 15 | background: #111111; 16 | } 17 | 18 | a { 19 | color: inherit; 20 | text-decoration: none; 21 | } 22 | -------------------------------------------------------------------------------- /managed-signers/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /managed-signers/src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | text-align: center; 9 | margin-top: 50px; 10 | } 11 | 12 | .btn { 13 | border: none; 14 | border-radius: 5px; 15 | transition: opacity 0.3s ease; 16 | padding: 10px 20px; 17 | font-size: 16px; 18 | } 19 | 20 | .btn:disabled { 21 | opacity: 0.5; 22 | } 23 | 24 | .btn:hover:not(:disabled) { 25 | opacity: 0.8; 26 | } 27 | 28 | .qrContainer { 29 | margin-top: 30px; 30 | display: flex; 31 | flex-direction: column; 32 | align-items: center; 33 | } 34 | 35 | .qr-label { 36 | margin-top: 20px; 37 | font-size: 16px; 38 | } 39 | 40 | .or { 41 | margin-top: 20px; 42 | font-size: 16px; 43 | } 44 | 45 | .castSection { 46 | margin-top: 30px; 47 | display: flex; 48 | flex-direction: column; 49 | align-items: center; 50 | width: 70%; 51 | } 52 | 53 | .link { 54 | color: #007bff; 55 | text-decoration: none; 56 | } 57 | 58 | .toast { 59 | position: fixed; 60 | bottom: 10px; 61 | left: 50%; 62 | transform: translateX(-50%); 63 | background-color: rgba(0, 0, 0, 0.7); 64 | color: white; 65 | padding: 10px 20px; 66 | border-radius: 5px; 67 | } 68 | 69 | .userInfo { 70 | margin-bottom: 30px; 71 | font-size: 20px; 72 | font-weight: bold; 73 | display: flex; 74 | justify-content: center; 75 | align-items: center; 76 | } 77 | 78 | .castContainer { 79 | display: flex; 80 | justify-content: center; 81 | align-items: start; 82 | gap: 10px; 83 | flex-direction: column; 84 | outline: none; 85 | width: 100%; 86 | } 87 | 88 | .castContainer:focus { 89 | outline: none; 90 | border: none; 91 | } 92 | 93 | .castTextarea { 94 | width: 100%; 95 | padding-right: 10px; 96 | } 97 | 98 | .profilePic { 99 | width: 60px; 100 | height: 60px; 101 | border-radius: 50%; 102 | margin-right: 10px; 103 | object-fit: cover; 104 | } 105 | -------------------------------------------------------------------------------- /managed-signers/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { DEFAULT_CAST, LOCAL_STORAGE_KEYS } from "@/constants"; 4 | import axios from "axios"; 5 | import QRCode from "qrcode.react"; 6 | import { useEffect, useState } from "react"; 7 | import styles from "./page.module.css"; 8 | import { User } from "@neynar/nodejs-sdk/build/neynar-api/v2"; 9 | import Image from "next/image"; 10 | 11 | interface FarcasterUser { 12 | signer_uuid: string; 13 | public_key: string; 14 | status: string; 15 | signer_approval_url?: string; 16 | fid?: number; 17 | } 18 | 19 | export default function Home() { 20 | const [loading, setLoading] = useState(false); 21 | const [farcasterUser, setFarcasterUser] = useState( 22 | null 23 | ); 24 | const [text, setText] = useState(""); 25 | const [isCasting, setIsCasting] = useState(false); 26 | const [showToast, setShowToast] = useState(false); 27 | const [user, setUser] = useState(null); 28 | 29 | useEffect(() => { 30 | const storedData = localStorage.getItem(LOCAL_STORAGE_KEYS.FARCASTER_USER); 31 | if (storedData) { 32 | const user: FarcasterUser = JSON.parse(storedData); 33 | setFarcasterUser(user); 34 | } 35 | }, []); 36 | 37 | useEffect(() => { 38 | if (farcasterUser && farcasterUser.status === "pending_approval") { 39 | let intervalId: NodeJS.Timeout; 40 | 41 | const startPolling = () => { 42 | intervalId = setInterval(async () => { 43 | try { 44 | const response = await axios.get( 45 | `/api/signer?signer_uuid=${farcasterUser?.signer_uuid}` 46 | ); 47 | const user = response.data as FarcasterUser; 48 | 49 | if (user?.status === "approved") { 50 | // store the user in local storage 51 | localStorage.setItem( 52 | LOCAL_STORAGE_KEYS.FARCASTER_USER, 53 | JSON.stringify(user) 54 | ); 55 | 56 | setFarcasterUser(user); 57 | clearInterval(intervalId); 58 | } 59 | } catch (error) { 60 | console.error("Error during polling", error); 61 | } 62 | }, 2000); 63 | }; 64 | 65 | const stopPolling = () => { 66 | clearInterval(intervalId); 67 | }; 68 | 69 | const handleVisibilityChange = () => { 70 | if (document.hidden) { 71 | stopPolling(); 72 | } else { 73 | startPolling(); 74 | } 75 | }; 76 | 77 | document.addEventListener("visibilitychange", handleVisibilityChange); 78 | 79 | // Start the polling when the effect runs. 80 | startPolling(); 81 | 82 | // Cleanup function to remove the event listener and clear interval. 83 | return () => { 84 | document.removeEventListener( 85 | "visibilitychange", 86 | handleVisibilityChange 87 | ); 88 | clearInterval(intervalId); 89 | }; 90 | } 91 | }, [farcasterUser]); 92 | 93 | const handleCast = async () => { 94 | setIsCasting(true); 95 | const castText = text.length === 0 ? DEFAULT_CAST : text; 96 | try { 97 | const response = await axios.post("/api/cast", { 98 | text: castText, 99 | signer_uuid: farcasterUser?.signer_uuid, 100 | }); 101 | if (response.status === 200) { 102 | setText(""); // Clear the text field 103 | displayToast(); // Show the toast 104 | } 105 | } catch (error) { 106 | console.error("Could not send the cast", error); 107 | } finally { 108 | setIsCasting(false); // Re-enable the button 109 | } 110 | }; 111 | 112 | const displayToast = () => { 113 | setShowToast(true); 114 | setTimeout(() => { 115 | setShowToast(false); 116 | }, 2000); 117 | }; 118 | 119 | const fetchUser = async () => { 120 | try { 121 | const response = await axios.get(`/api/user?fid=${farcasterUser?.fid}`); 122 | setUser(response.data); 123 | } catch (error) { 124 | console.error("Could not fetch the user", error); 125 | } 126 | }; 127 | 128 | useEffect(() => { 129 | if (farcasterUser?.status === "approved") { 130 | fetchUser(); 131 | } 132 | }, [farcasterUser]); 133 | 134 | return ( 135 |
136 | {!farcasterUser?.status && ( 137 | 144 | )} 145 | 146 | {farcasterUser?.status == "pending_approval" && 147 | farcasterUser?.signer_approval_url && ( 148 | 160 | )} 161 | 162 | {farcasterUser?.status == "approved" && ( 163 |
164 |
165 | {user?.pfp_url && ( 166 | {user?.display_name 173 | )} 174 | Hello {user?.display_name} 👋 175 |
176 |
177 |