Farcaster has grown exponentially in the last few months, and to truly scale and bear the load there needs to be more clients. Fetching casts and populating a feed isnât too difficult, but writing to the Farcaster network is another issue. There have been several ways authentication has evolved in Farcaster, and the best approach so far has been signer keys. These are similar to API keys, where the user can approve or revoke them from posting on their behalf, and weâve written about how you could do this yourself. The only downside is actually handling that signer key once itâs created.
One approach thatâs been used before is storing it in local storage on the client, such as web browser cookies. This is actually how we approached it when building a photo client for Farcaster, and one of the biggest complaints was trying to sign in with different devices or having that cache cleared. When that happened, the user had to pay warps to sign in again, which made it less than desirable to use. Some people have opted to storing those keys on their own database, but that can come with its own risks.
Pinata saw this need and has recently just released Farcaster Auth, a service that manages signers for Farcaster Apps. With our API and FDK, adding auth and sending casts within your own Farcaster client is a breeze. In this tutorial, weâll show you how to set it up in Next.js and send a âHello Worldâ cast at the end!
Setup
In order to follow this tutorial you will need a few things to get up and running.
Pinata Account
Farcaster Auth is a new paid feature starting on the Picnic plan, which also gives you access to all sorts of stuff you can use to build your client like image or video support with IPFS. If you donât have a paid account, sign up today (and shoot me a message if youâre serious and want a discount đ). Once you have an account set up, youâll want to follow these steps to create an API key (specifically the JWT).
Farcaster Account
One of the things the FDK will be handling for you under the hood is creating and signing ED25519 keys. However, to sign those keys, you will need a Farcaster account - specifically the mnemonic phrase for the account and the FID. Usually, when youâre building a Farcaster app, the app itself will have its own account (e.g. @photocast). However, you do not necessarily have to do that and you could use your own Farcaster account. To find your mnemonic phrase, you can navigate to the Warpcast app, and go to Settings > Advance > Reveal recovery phrase. Keep in mind, that youâll need to keep this phrase highly confidential; the Pinata team will never ask for this, and our FDK only uses it locally to sign the keys.
Stack
As stated earlier, we will be using Next.js to build this tutorial, so make sure you have Node.js installed along with a good text editor. Once you have everything mentioned beforehand ready to go, letâs run the following command to start up the project.
npx create-next-app@latest farcaster-auth
Weâll select all the default options, and once its done installing, weâll cd into the repo and install some other dependencies.
cd farcaster-auth && npm install pinata-fdk @farcaster/auth-kit react-qrcode-logo
With everything installed, letâs create a new .env.local file at the root of the project and add the following variables.
# Your pinata API key
JWT PINATA_JWT=
# The App FID that the user is signing into
APP_FID=
# The App mnemonic phrase that the user is signing into
DEVELOPER_MNEMONIC=
The Pinata JWT would be the API key we made earlier, and the App FID & Developer Mnemonic is from the Farcaster Account that will be signing the keys. With all of that set-up, we can now open the project and start building this!
Creating and Storing a Signer
Before we start creating a signer, weâre going to implement the Farcaster Authkit, provided by the team who built Farcaster. Itâs a handy library that can let users scan a QR code and sign in with Farcaster. Now, that may sound like creating a signer, but in reality itâs just providing the app the userâs FID and other read-only info. Nevertheless, it is helpful to find someone by FID and make sure they are the owner of that FID when fetching a signer down the road.
To set it up, open the app/page.tsx file and make the following changes.
"use client";
import "@farcaster/auth-kit/styles.css";
import { AuthKitProvider, SignInButton } from "@farcaster/auth-kit";
const config = {
rpcUrl: "https://mainnet.optimism.io",
domain: "example.com",
siweUri: "https://example.com/login",
};
export default function Home() {
return (
<AuthKitProvider config={config}>
<main className="flex min-h-screen flex-col items-center justify-center gap-4 p-24 w-full">
<SignInButton
onSuccess={({ fid, username }) =>
console.log(
`Hello, ${username}! Your fid is ${fid}.`
)
}
/>
</main>
</AuthKitProvider>
);
}
If you run npm run dev in the terminal you should get the page with a button to sign in. Go ahead and try it out to see how it works. Now, with that done, weâll make a new API route where we will create a signer. Create the path app/api/signer/route.ts and open the route.ts file, then put in the following code.
import { NextResponse, NextRequest } from "next/server";
import { PinataFDK } from "pinata-fdk";
const fdk = new PinataFDK({
pinata_jwt: process.env.PINATA_JWT as string,
pinata_gateway: "",
app_fid: process.env.APP_FID as string,
app_mnemonic: process.env.DEVELOPER_MNEMONIC,
});
export async function POST() {
try {
const res = await fdk.createSponsoredSigner();
return NextResponse.json(res);
} catch (error) {
console.log(error);
return NextResponse.json(error);
}
}
As you can probably see, the FDK makes a lot of this really simple, abstracting pages of code for us. All we have to do is create the fdk instance with our pinata_jwt, the app_fid, and the app_mnemonic. Then we just make a POST endpoint where we call fdk.createSponsoredSigner() and return the results. We also have the option to use createSigner instead where the end user would have to pay warps, but with createSponsoredSigner that is simplified for the end user.
Back in app/page.tsx letâs make our client-side function to call the endpoint.
"use client";
import "@farcaster/auth-kit/styles.css";
import { AuthKitProvider, SignInButton } from "@farcaster/auth-kit";
import { QRCode } from "react-qrcode-logo";
import { useState } from "react";
const config = {
rpcUrl: "https://mainnet.optimism.io",
domain: "example.com",
siweUri: "https://example.com/login",
};
export default function Home() {
const [deepLink, setDeepLink]: any = useState();
const [openQR, setOpenQR] = useState(false);
const [fid, setFid]: any = useState();
const [signerId, setSignerId]: any = useState();
async function createSigner() {
// start the process of creating a new signer
try {
// send API request to create signer with Pinata
const signerReq = await fetch(`/api/signer`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const signerRes = await signerReq.json();
// Set deep link url for user to scan and open the qr code
setDeepLink(signerRes.deep_link_url);
setOpenQR(true);
} catch (error) {
console.log(error);
}
}
return (
<AuthKitProvider config={config}>
<main className="flex min-h-screen flex-col items-center justify-center gap-4 p-24 w-full">
<SignInButton
onSuccess={({ fid, username }) =>
console.log(
`Hello, ${username}! Your fid is ${fid}.`,
setFid(fid)
)
}
/>
{!signerId && fid && (
<button
className="h-10 px-4 py-2 bg-black text-white hover:bg-black/90 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
onClick={createSigner}
>
Create Signer
</button>
)}
{openQR && (
<QRCode
value={deepLink}
size={200}
logoImage="https://dweb.mypinata.cloud/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng"
logoWidth={50}
logoHeight={50}
logoPadding={5}
logoPaddingStyle="square"
qrStyle="dots"
eyeRadius={15}
/>
)}
</main>
</AuthKitProvider>
);
}
Now, weâve added several different things here, so letâs go over them. First, we added some react state to handle our QR code opening and deepLinkUrl, as well as the userâs fid and signerId when we get there. In our createSigner() function, it will make an API call to our signerâs endpoint, use the end result to set our deepLinkUrl, and open the QRCode/> component. Whatâs happened so far is that our FDK has created a keypair and signed it, and then sent a request to Pinata which then sends it to Warpcast for approval. At that point, we get that deepLinkUrl back which the user can open with their mobile device via the QR code. This will open Warpcast for the user to approve the signer or not. While this is all happening, weâre going to need to poll whether that user has approved the key or not. To do that, letâs create a new API route under app/api/poll/route.ts.
import { NextResponse, NextRequest } from "next/server";
import { PinataFDK } from "pinata-fdk";
const fdk = new PinataFDK({
pinata_jwt: process.env.PINATA_JWT as string,
pinata_gateway: "",
app_fid: process.env.APP_FID as string,
app_mnemonic: process.env.DEVELOPER_MNEMONIC,
});
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const token: any = searchParams.get("token");
const res = await fdk.pollSigner(token);
return NextResponse.json(res);
} catch (error) {
console.log(error);
return NextResponse.json(error);
}
}
This one, again, is really simple. We just make our FDK instance and then use the search params to get the polling token. With that, we can use fdk.pollSigner() with the token and return the results. Back in our page.tsx file, weâll make some additions to createSigner().
async function createSigner() {
// start the process of creating a new signer
try {
// send API request to create signer with Pinata
const signerReq = await fetch(`/api/signer`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
const signerRes = await signerReq.json();
// Set deep link url for user to scan and open the qr code
setDeepLink(signerRes.deep_link_url);
setOpenQR(true);
// While the user is polling up the approval in warpcast, poll the token and create a while loop to keep checking if its complete. Set a timeout in the even it fails
const pollReq = await fetch(`/api/poll?token=${signerRes.token}`);
const pollRes = await pollReq.json();
const pollStartTime = Date.now();
while (pollRes.state != "completed") {
if (Date.now() - pollStartTime > 120000) {
console.log("Polling timeout reached");
alert("Request timed out");
setOpenQR(false);
break;
}
const pollReq = await fetch(`/api/poll?token=${signerRes.token}`);
const pollRes = await pollReq.json();
if (pollRes.state === "completed") {
// If the approval is successful set the signerId and store it to local storage
setDeepLink(null);
setOpenQR(false);
setSignerId(signerRes.signer_id);
localStorage.setItem("signer_id", signerRes.signer_id);
return pollRes;
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
} catch (error) {
console.log(error);
}
}
While the user is working on opening the QR code, weâre going to immediately start polling. Weâll get an initial request sent, and then start a while loop with a delay. If the state is not completed, then weâll continue to fetch our endpoint with the polling token, using a 2-second delay so we donât overdo it. Weâll also set a time-out limit of 2 minutes, where if the user does not complete the approval, weâll restart and close the modal out. If / when the user approves the signer, and itâs completed, weâll close the QR code, set the signerId state in our app, and store the signerId in local storage. If you want more persistent auth methods - such as Next Auth - you could implement them here.
Persisting a Signer
Now all of this is great, but what if someone clears their browser cache or logs in from another device? This is where Farcaster Auth really shines. Weâll create a new GET function under app/api/signer/route.ts to fetch by FID a previously created signer.
import { NextResponse, NextRequest } from "next/server";
import { PinataFDK } from "pinata-fdk";
const fdk = new PinataFDK({
pinata_jwt: process.env.PINATA_JWT as string,
pinata_gateway: "",
app_fid: process.env.APP_FID as string,
app_mnemonic: process.env.DEVELOPER_MNEMONIC,
});
export async function POST() {
try {
const res = await fdk.createSponsoredSigner();
return NextResponse.json(res);
} catch (error) {
console.log(error);
return NextResponse.json(error);
}
}
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const fid: any = searchParams.get("fid");
const res = await fdk.getSigners(fid);
console.log(res)
return NextResponse.json(res);
} catch (error) {
console.log(error);
return NextResponse.json(error);
}
}
Then, back on the client side, weâll create a new function called checkStorage().
async function checkStorage() {
// We an check if there happens to already be a signer in local storage, and if not we can fetch one we've already created via pinata
try {
if (typeof window != "undefined") {
const signer = localStorage.getItem("signer_id");
console.log("local storage: ", signer);
if (signer != null) {
setSignerId(signer);
} else {
const signerReq = await fetch(`/api/signer?fid=${fid}`);
const signerRes = await signerReq.json();
if (signerRes.signers.length > 0) {
console.log("signer found and set")
setSignerId(signerRes.signers[0].signer_uuid);
} else {
console.log("no signer found")
return;
}
}
}
} catch (error) {
console.log(error);
}
}
This is a function weâll fire as soon as the user signs in with the auth kit. Weâll first check to see if there is a signer in local storage, and if there is, weâll set it in the signerId state. If there isnât one in local storage, weâll use our API route to check with their FID if we have one saved with Pinata. If not, weâll make them create a new one. Now, we can go into our markup to trigger this upon sign-in!
return (
<AuthKitProvider config={config}>
<main className="flex min-h-screen flex-col items-center justify-center gap-4 p-24 w-full">
<SignInButton
onSuccess={({ fid, username }) =>
console.log(
`Hello, ${username}! Your fid is ${fid}.`,
setFid(fid),
checkStorage(),
)
}
/>
{!signerId && fid && (
<button
className="h-10 px-4 py-2 bg-black text-white hover:bg-black/90 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
onClick={createSigner}
>
Create Signer
</button>
)}
{openQR && (
<QRCode
value={deepLink}
size={200}
logoImage="https://dweb.mypinata.cloud/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng"
logoWidth={50}
logoHeight={50}
logoPadding={5}
logoPaddingStyle="square"
qrStyle="dots"
eyeRadius={15}
/>
)}
</main>
</AuthKitProvider>
);
Sending a Cast
Weâve gotten this far, why not send a cast with our new signerId? Youâll be amazed at how simple it is. Letâs create a new API route called api/cast/route.ts.
import { NextResponse, NextRequest } from "next/server";
import { PinataFDK } from "pinata-fdk";
const fdk = new PinataFDK({
pinata_jwt: process.env.PINATA_JWT as string,
pinata_gateway: "",
app_fid: process.env.APP_FID as string,
app_mnemonic: process.env.DEVELOPER_MNEMONIC as string,
});
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const res = await fdk.sendCast({
castAddBody: {
text: "Hello World!",
parentUrl: "https://warpcast.com/~/channel/pinata"
},
signerId: body.signerId
});
return NextResponse.json(res);
} catch (error) {
console.log(error);
return NextResponse.json(error);
}
}
Just like before, weâll create an fdk instance with our environment variables, then make a POST function. In it, weâll parse the body of the request, then call fdk.sendCast(). Inside that function, weâll declare our castAddBody, which is all the contents of our cast, and include the signerId. Thereâs a lot you can add here, so definitely check out the docs here to see what you could cast instead!
Back on the client side, weâll create a new function to send the cast and a button in the jsx to show if we have an approved signer.
async function sendCast() {
// Using our already created signerId we can send a cast with our API route
try {
const castReq = await fetch(`/api/cast`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ signerId: signerId }),
});
const castRes = await castReq.json();
console.log(castRes);
alert("Cast sent!");
} catch (error) {
console.log(error);
}
}
return (
<AuthKitProvider config={config}>
<main className="flex min-h-screen flex-col items-center justify-center gap-4 p-24 w-full">
<SignInButton
onSuccess={({ fid, username }) =>
console.log(
`Hello, ${username}! Your fid is ${fid}.`,
setFid(fid),
checkStorage(),
)
}
/>
{!signerId && fid && (
<button
className="h-10 px-4 py-2 bg-black text-white hover:bg-black/90 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
onClick={createSigner}
>
Create Signer
</button>
)}
{openQR && (
<QRCode
value={deepLink}
size={200}
logoImage="https://dweb.mypinata.cloud/ipfs/QmVLwvmGehsrNEvhcCnnsw5RQNseohgEkFNN1848zNzdng"
logoWidth={50}
logoHeight={50}
logoPadding={5}
logoPaddingStyle="square"
qrStyle="dots"
eyeRadius={15}
/>
)}
{signerId && (
<button
className="h-10 px-4 py-2 bg-black text-white hover:bg-black/90 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
onClick={sendCast}
>
Cast "Hello World!" to /pinata
</button>
)}
</main>
</AuthKitProvider>
);
If you wait a few seconds, then you should be able to see the cast on Warpcast! If you want to view the entire code for this tutorial or use it as a template, then you can view it here.
Wrapping Up
Farcaster Auth provides the foundation needed for clients, but it doesnât stop there - be sure to check out our docs on how you can use the Farcaster API to fetch casts and create feeds to build a fully-fledged Farcaster client. Then go the extra mile and add image or video uploads with IPFS by following this guide. Whatever you build, be sure to share it on the Pinata Farcaster Channel.
Happy Casting!
![Pinata logo](/_next/image?url=https%3A%2F%2Fstorage.googleapis.com%2Fpapyrus_images%2Fd21a2d8fbde31bd3282d8b5a7919820d.png&w=384&q=75)