llms.txt

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:

  1. Read the full snap spec
  2. Generate valid snap code
  3. 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.