Build a Farcaster Frame V2 from scratch

Share
Build a Farcaster Frame V2 from scratch

We recently ported our Telegram mini-app to a Farcaster Frame v2. Since our game is build on Elixir using Phoenix LiveView, we cannot easily use the React SDK Provided by Farcaster. This post highlights some challenges along with solutions - hopefully this is useful to someone else deploying Frames v2 from scratch.

Specification

The obvious place to start if you're not using the React npm package is to check out the specification. It was super useful to figure out how to create the domain manifest and the embed JSON meta tags. Those are handled for you in the sample code bases provided by Farcaster, but if you're starting from scratch you're on your own.

Embed tag

This goes in the <head> of your html page and is a stringified version of this JSON object:

{
  "version": "next",
  "imageUrl": "https://#{host}/images/image.png",
  "button": {
    "title": "đŸ•šī¸ Play",
    "action": {
      "type": "launch_frame",
      "name": "Funstash Game",
      "url": "https://#{host}/farcaster",
      "splashImageUrl": "https://#{host}/images/splash.png",
      "splashBackgroundColor": "#fff"
    }
  }
}

<meta name="fc:frame" />

This allows clients like Warpcast to read the fc:frame tag and display the link pointing to your frame v2 nice and styled.

Domain Manifest

This was the first big gotcha. I had to dig quite a bit to figure out how to sign the accountAssociation part. I first looked up the JSON Farcaster Signature format and looked up the reference implementation, I then tried to associate a wallet address to my account and try to sign from that wallet in my terminal.

{
    "frame": {
        "name": "Funstash",
        "version": "1",
        "buttonTitle": "Play Funstash",
        "homeUrl": "https://app.funstash.fun/farcaster",
        "iconUrl": "https://app.funstash.fun/images/icon.png",
        "imageUrl": "https://app.funstash.fun//images/logo.jpeg",
        "splashBackgroundColor": "#fff",
        "splashImageUrl": "https://app.funstash.fun/images/splash.png",
        "webhookUrl": "https://app.funstash.fun/api/webhook"
    },
    "accountAssociation": {
        "header": "eyJmaWQiOjgxMzksInR5cGUi...",
        "signature": "MHg2NTA2YTkyZjVkMmFjMTliZWQ1Y...",
        "payload": "eyJkb21haW4iOiJhcHAuZnVuc3Rhc2guZnVuIn0"
    }
}

~/.well-known/farcaster.json

Turns out it's a lot easier than that, but you do need to do it from your Warpcast mobile client. First, you need to enable Developer mode in the app. You'll find it by going to your profile, clicking the gearbox icon, then navigating to Advanced. Or you can click on this link from your mobile phone and it will open the Domains page directly:

https://warpcast.com/~/developers/domains

From that page you can generate the domain manifest (just the accountAssociation part or the whole thing). You'll need one farcaster.json file for each domain you are serving the frame from.

Core SDK

In order to initialize the frame, you'll need to install the @farcaster/frame-sdk npm package. This is roughly how we use it in our app:

import sdk from '@farcaster/frame-sdk';

document.addEventListener("DOMContentLoaded", async () => {
  sdk.actions.ready();

  window.sdk = sdk;
  let context = await sdk.context;

  await sdk.actions.addFrame()
  
  let nonce = document.getElementById('siwe-nonce').value;
  let message = await sdk.actions.signIn({nonce: nonce});
  
  const queryString = new URLSearchParams(message).toString();
  const user = new URLSearchParams(context.user).toString();
  const client = new URLSearchParams(context.client).toString();
  
  window.location.href = `/auth?${queryString}&${user}&${client}`;
})

app.js

We first call sdk.actions.ready() after DOM is ready. This was my first mistake, I had missed that line in the specification docs and my frame kept displaying a blank page. We then pass the sdk context along with the SIWE (Sign in with ethereum) signed payload to our backend (the /auth route). After we verify the signature we just issue a cookie so that the rest of the integrations with the frame do not require authentication.

You'll note that we also call sdk.actions.addFrame to prompt the user to bookmark the frame into their Frames page.

The Phoenix Framework uses esbuild to bundle Javascript files but otherwise does not use any frameworks. One could of course bring in React or Vue, but it would defeat the whole purpose of using LiveView.

Elixir uses the actor model concurrency to its core and this paradigm has permeated into how front-end applications are built. Each LiveView is a process that can send and receive messages asynchronously (on the client as well as the server). The client is connected to the server via websockets and changes are processed on the backend and sent over the wire (only diffs in order to reduce the bandwidth). This type of architecture allows you to build things like multi-player, real time games much easier than in any other tool out there.

Phoenix Websockets and iFrames

Warpcast allows you to access everything from your browser by logging in with your mobile Warpcast client. You can also debug your frames like that - the frame is loaded in an iFrame and you can get access to the Developer console. This can come in handy for catching errors in your JS code.

Phoenix will add an x-frame-options response header and set it to sameorigin . This will not load your frame if you're debugging or trying to open it on Warpcast desktop. This is how we fixed that problem:

def allow_iframe(conn, _opts) do
    conn
    |> delete_resp_header("x-frame-options")
    |> put_resp_header(
      "content-security-policy",
      "frame-ancestors 'self' https://warpcast.com https://app.funstash.fun"
    )
end

router.ex

The other problem with embedding a LiveView page using iFrame is setting cookies for persisting sessions. It seems like a common problem people have with this, so the solution was to lax the SameSite cookie options. We added this to our

Webhooks

Once everything is in place and the frame loads, we had to handle a few events from Farcaster via webhooks. We send the user notifications when a game ends (for now) and if the user added the Frame to their home screen, they can receive those notifications. They can also turn them off. Responding to those webhooks is easy. Farcaster will post to the webhookUrl you configured in the domain manifest:

{
    "frame": {
        ...
        "webhookUrl": "https://app.funstash.fun/api/webhook"
        ...
    },
    "accountAssociation": {}
}

All requests are signed using EdDSA (Ed25519). There's a handy Elixir package for that called ed25519. This is how we handle enabling notifications for a user when they either: add the frame to their home screen or enable notifications:

  # {
  #  "event": "frame_added",
  #  "notificationDetails": {
  #    "url": "https://api.warpcast.com/v1/frame-notifications",
  #    "token": "a0...."
  #  }
  # }
  # {
  #  "event": "notifications_enabled",
  #  "notificationDetails": {
  #    "url": "https://api.warpcast.com/v1/frame-notifications",
  #    "token": "a0...."
  #  }
  # }
  defp handle_farcaster_webhook(fid, %{"event" => event} = payload)
       when event in ["frame_added", "notifications_enabled"] do
    with %{"notificationDetails" => %{"url" => url, "token" => token}} <- payload,
         %User{} = user <- Users.get_user_by_fid(fid) do
      tg_data =
        Map.merge(user.tg_data, %{
          "farcaster_notifications" => %{
            "enabled" => true,
            "url" => url,
            "token" => token
          }
        })

      Users.update_user(user, %{"tg_data" => tg_data})
    else
      {:error, _} ->
        :noop
    end
  end

These are all the events that are sent as of the time of this writing:

  • Frame added (frame_added)
  • Frame removed (frame_removed)
  • Enable notifications (notifications_enabled)
  • Disable notifications (notifications_disabled)