I was just scrolling on Farcaster and this cast scrolled onto my feed:

The cast that gave me the idea to build Friendcaster.
I clicked on the link to see what Chirpty was, and felt that this would be a perfect project for me. It’s simple, but not too simple.
How it Works
All the code is available on Github
This is the root dir of my project. I use typescript so I had to make some changes to package.json
and tsconfig.json
but the basic initialization was done via npm init -y
.
├── index.ts
├── src
│ ├── api.ts
│ ├── data.ts
│ ├── final.ts
│ └── image.ts
└── views
├── pages
│ ├── circle.ejs
│ ├── faq.ejs
│ └── index.ejs
└── partials
├── footer.ejs
├── head.ejs
└── header.ejs
├── public
│ ├── circle.png
│ └── favicon.ico
├── .env
├── package.json
├── tsconfig.json
The code is split into multiple files for better maintainability:
index.ts
- this is the entry point into the application. It sets up the server (Express.js) and the relevant routessrc/api.ts
- this contains a bunch of helper functions to fetch data from the smart contracts on the blockchain.src/data.ts
- this file processes the data we fetch to make it easier to make images using these.src/image.ts
- code to create image out of the data.src/final.ts
- a bunch of functions and constants to tie everything together and offer a single endpoint for our server to call into.
Fetching Data
src/api.ts
We’ll need to understand a bit about how Farcaster stores user data, to understand the calls we make. Farcaster allows users to store their casts (tweets) on whatever server they wish to. The server could be AWS s3, a VPS on Digital Ocean or a Raspberry Pi under their desk. This keeps the cost of posting low (cuz storing data on the blockchain is high). But to incentivize the servers to not misbehave, users are able to switch the server that stores the data. The user just need copy their data to the new server and change the url in the Farcaster Name Registry - a smart contract that maps an address -> username -> url of the host directory.
So to fetch a user’s posts we need to query the smart contract, with the username, to get the url of the server that stores their data, then query the data from that url.
I’m using ethers.js
to interact with the blockchain, and Alchemy as a node service.
To get the url of a user’s host directory, we call the getDirectoryUrl
function defined in the Name Registry Contract with the username formatted in bytes32 string
. Then we get the data stored in the host directory using
got()
.
export const getDirectory = async (username: string, registryContract:any): Promise<IDirectory> => {
const byte32Name = utils.formatBytes32String(username);
const directoryUrl = await registryContract.getDirectoryUrl(byte32Name);
const directoryResponse = await got(directoryUrl, { protocol: 'https:' });
const directoryBody = JSON.parse(directoryResponse.body).body;
return {
directoryUrl,
directoryBody
};
}
The data stored in the host directory is of the shape:
{
body: {
addressActivityUrl: 'http://www.host.xyz/bob/casts.json',
avatarUrl: 'https://github.com/bob_the_builder.png',
displayName: 'Bob',
proofUrl: 'http://www.host.xyz/bob/proof.json',
timestamp: 1624314341272,
version: 1,
},
merkleRoot: string,
signature: string,
}
I’ve defined two more functions in this file is getAvatarUrl, getAvatarListUrl
. They fetch the avatar url of a user and a list of users, respectively. They call getDirectory
extract the url from the directory blob.
const { directoryBody } = await getDirectory(username, registryContract);
const avatarUrl = directoryBody.avatarUrl;
Processing that data to look for posts that are replies
src/data.ts
Farcaster stores all of user’s posts in the directory as a json file, that is referenced by the directoryBody.addressActivityUrl
. The json file is shaped like this:
interface IAction {
body: Object;
merkleRoot: string;
signature: string;
meta: {
displayName: string;
avatar: string;
isVerifiedAvatar: boolean;
numReplyChildren: number;
reactions: Object;
recasts: Object;
watches: Object;
replyParentUsername?: {
address: string;
username: string;
}
}
}
Reply posts have the replyParentUsername
key defined while normal posts don’t have this key cuz they are the parent. To filter posts that are replies vs the posts that are just normal posts we define this filter on an array containing all the user’s timeline. We then map over the filtered array to fetch the username of the post our user replied to.
const addressActivity: IAction[] = JSON.parse(addressActivityResponse.body);
const repliedPeople: string[] = addressActivity.filter((action:IAction) => {
// check if the action is a associated with another user.
if (!action.meta.replyParentUsername) {
return false;
}
return true;
}).map((action:any) => action.meta.replyParentUsername.username)
Now we have the username of everyone that we replied to. The final circle contains several rings, which represent our frequency of interaction. We need to add weights to the array we have.
const interactionFrequency: IFreqObj = interactedPeople.reduce((prevValue: any, currentValue: string) => {
return prevValue[currentValue] ? ++prevValue[currentValue] : prevValue[currentValue] = 1, prevValue
}, {});
const tally: INameFreq[] | any[] = []
for (const [uname, freq] of Object.entries(interactionFrequency)) {
tally.push({
username: uname,
freq: freq
});
}
tally.sort((a:any, b:any) => b.freq - a.freq);
We add weight to our array of usernames using the reduce function. We then sort the array in the tally array.
Next we add the caller’s username to the list, and the append portions of the array into a result
array. This is mostly to handle the data for different rings separately.
head = [{username: username, freq: 0, avatarUrl: userUrl}, ...head];
let result: INameFreq[][] = []
result.push(head.splice(0, 1))
result.push(head.splice(0, 8))
result.push(head.splice(0, 15))
result.push(head.splice(0, 26))
return result;
Generating the Image
src/image.ts
I basically copied the image generation style from
Chirpty
which uses
node-canvas
. MY render
function accepts a config object of interface IConfig
and writes the generated image to public/circle.png
. Since I’m running Express.js I can set that image as my static directory and access the image from anywhere.
interface IConfig {
users: INameFreq[];
count: number;
radius: number;
distance: number;
}
Tying everything into a single endpoint for the server:
src/final.ts
This file contains the contract address and the contract ABI which are required to generate a contract instance. This file instantiates the contract and calls the getInteractionFrequency
with it. then supplies the data to the render
function.
export async function createImage(username: string) {
const provider = new providers.AlchemyProvider('rinkeby', process.env.ALCHEMY_API_KEY);
const registryContract = new Contract(REGISTRY_CONTRACT_ADDRESS, REGISTRY_ABI, provider);
const block = await provider.getBlockNumber();
console.log("Block: ", block);
const data = await getInteractionFrequency(username, registryContract)
await render([
{distance: 0, count: 1, radius: 110, users: data[0]},
{distance: 300, count: 8, radius: 80, users: data[1]},
{distance: 400, count: 15, radius: 50, users: data[2]},
{distance: 500, count: 26, radius: 30, users: data[3]},
]);
};
Setting up Express.js in
index.ts
It contains generic code to setup some basic routes, and public directories. Only one route is of relevance here. It is the route that returns the interaction circle image.
app.get("/circle/:username", (req, res, next) => {
let username = req.params.username;
if (username[0] == '@') username = username.slice(1,);
createImage(username)
.then(e => {
setTimeout(() => (res.render('pages/circle'), 1))
}).catch(next)
});
When the user enters their username in the form (on the homepage), they are sent to the /circle/<username>
route. This will trigger the function createImage
, defined in src/final.ts
. After the image has been written to public/circle.png
file, we tell Express.js to render the page using /views/pages/circle
view. The view contains an image tag which renders the interaction circle.
Conclusion
Friendcaster was the first project that people other than my irl family used. It was a real product. That helped people. And the response from the fc community was awesome. I got some really good feedback and feature ideas, and I feel that it would be a good idea to launch a version 2 of friendcaster.
- Consider other interactions (likes, recasts) in the calculation.
- Build a full-fledged frontend in React (maybe use Next.js). And offload image rendering here.
- Allow users to customize stuff: bg, profile outline, size etc.
- Use an outward spiral instead of multiple levels of circles.
- Replace the static canvas image with a dynamic animation thingy built via react
- Mint as nft. Store image on Arweave/IPFS.
- Hovering a profile should show who it is etc.
- Integrations with other Farcaster apps
- farcasternews.xyz to show karma.
- @perl to show collections etc.
- Serverless?