Building a Snap
There are several ways to create a Farcaster Snap, from AI-assisted generation to manual implementation.
Designing for the feed
Snaps render at a fixed width (~480px) inside Farcaster client feeds. Most clients clip snap content at around 500px in height — anything below that line is not visible to users without scrolling or expansion (behavior varies by client).
Design your snap's primary content to fit within this visible area. Use the Emulator to preview your snap — it shows a 500px height indicator so you can see where content will be clipped.
Agent Skill
If you use a coding agent like Claude Code, you can ask it to install a skill that generates snaps from natural language:
install the farcaster-snap skill from https://docs.farcaster.xyz/snap/SKILL.md
make me a Farcaster Snap poll asking users to pick their favorite variety of mole
The skill will:
- Read the full snap spec
- Generate valid snap code
- Deploy it to a live URL
This is the fastest way to go from idea to working snap.
Template (Hono)
The snap-template/ directory is a starter project using Hono with
the @farcaster/snap-hono package:
# From the repo root
cp -r snap-template my-snap
cd my-snap
pnpm install
Edit src/index.ts to implement your snap logic:
import { Hono } from "hono";
import { registerSnapHandler } from "@farcaster/snap-hono";
const app = new Hono();
registerSnapHandler(app, async (ctx) => {
if (ctx.action.type === "get") {
return {
version: "2.0",
theme: { accent: "purple" },
ui: {
root: "page",
elements: {
page: {
type: "stack",
props: {},
children: ["title", "body", "action"],
},
title: {
type: "text",
props: { content: "My Snap", weight: "bold" },
},
body: {
type: "text",
props: { content: "Hello world" },
},
action: {
type: "button",
props: { label: "Refresh", variant: "primary" },
on: {
press: {
action: "submit",
params: { target: "https://my-snap.com/" },
},
},
},
},
},
};
}
// Handle POST interactions
const { user, inputs } = ctx.action;
const fid = user.fid;
const url = new URL(ctx.request.url);
const action = url.searchParams.get("action"); // e.g. ?action=vote
// ... your logic here
});
Run locally:
SKIP_JFS_VERIFICATION=1 pnpm dev # http://localhost:3003
Testing
Use the Emulator to test your snap. Enter your snap's URL and interact with it -- the emulator signs messages automatically, so no signature bypass is needed.
Deploying
Snaps can be deployed anywhere that serves HTTP. Common options:
- Vercel -- works with the Hono template out of the box
- Any Node.js host -- the Hono template includes a standalone server
Set SNAP_PUBLIC_BASE_URL to your deployment origin (no trailing slash) so button
target URLs resolve correctly.
After deploying, verify your snap works:
curl -sS -H 'Accept: application/vnd.farcaster.snap+json' https://your-snap-url.com/
You should get valid JSON with content type application/vnd.farcaster.snap+json.
Common pitfalls
Missing .js on local imports
The template is an ESM project ("type": "module"). All relative imports must include
the .js extension, even though the source files are .ts:
// ✅ correct
import { foo } from "./foo.js";
// ❌ wrong — breaks on deploy with `500 FUNCTION_INVOCATION_FAILED`
import { foo } from "./foo";
Omitting the extension is a silent trap: tsx accepts bare imports in local dev, and the
TypeScript "bundler" module resolution used to accept them at typecheck too — but the
Vercel Edge / Node ESM runtime enforces the extension, so every route returns
500 FUNCTION_INVOCATION_FAILED after deploy. The current template's tsconfig.json
uses "moduleResolution": "NodeNext" so pnpm build (tsc --noEmit) now catches this
at build time.
Bare package imports (hono, @farcaster/snap, etc.) do not need an extension.