๐Ÿ•ธ๏ธOnchain Graph

Learn how to use Airstack to display the Onchain Graph of users.

The Onchain Graph is the web3 address book. It analyzes all of a user/wallet's onchain interactions and recommends contacts based on their strengh of relationship. It currently brings together all of the user's onchain interactions & tokens in common across POAPs, NFTs, token transfers, and Lens and Farcaster.

Developers are utilizing Onchain Graph for recommendation engines, pre-populating friends lists, address books, spam filters, product enhancements, and more.

Live Demo

We have integrated onchain graph into the Airstack Explorer as you can see below. In Airstack Explorer you can enter any 0x address, Lens, Farcaster, or ENS and get the user's onchain graph.

To try it out yourself, click here:

betashop.eth's onchain graph

Table Of Contents

In this tutorial, you'll learn how to build an onchain graph for your web3 social application using either JavaScript or Python.

Currently, Airstack Explorer's onchain graph implementation has no backend and hence it takes time to scan and fetch all the data.

For backend integrations, it is best practice that you take the following approach for the best user experience:

  1. fetch your users' onchain graph data periodically (e.g. once a day) as a cronjob

  2. store your user's onchain graph data into your preferred database

  3. Fetched the data from your frontend and cache it

With this approach, your user shall receive their onchain graph data almost instantaneously instead of calling the API on-demand which could take minutes.

In the future, we shall provide webhooks and a dedicated Onchain Graph API for lighter-weight integrations.

The algorithm for building onchain graph will be as follows:

Pre-requisites

  • An Airstack account

  • Basic knowledge of GraphQL

Get Started

To get started, install the Airstack SDK:

React

npm install @airstack/airstack-react

Node

npm install @airstack/node

Step 1: Fetch All Onchain Graph Data

In order to build a comprehensive onchain graph of a user, it'll require various kinds of data to analyze. Those data comprises of:

  • common POAP holders that are also attended by the user

  • Lens and Farcaster social followers and following of the given user

  • Token transfers senders and receivers

  • common Ethereum, Base, and Zora NFT holders that are also held by the user

For token transfers and common NFT holders, you can include other Airstack-supported chains.

In this step, you'll learn to fetch all the data that you need to build the onchain graph of a user.

Step 1.1: Fetch Common POAP Holders Data

In order to fetch the common POAP holders that hold the POAPs attended by a given user, it will require 2 steps:

Fetch all non-virtual POAPs' event IDs owned by a user

You can use Airstack to fetch all the POAPs that are hold by a given user, e.g. vitalik.eth, and check if the events are non-virtual or not:

Demo

Show me all POAPs owned by vitalik.eth with their event IDs and whether they are virtual or not

Code

query MyQuery($user: Identity!) {
  Poaps(input: { filter: { owner: { _eq: $user } }, blockchain: ALL }) {
    Poap {
      eventId
      poapEvent {
        isVirtualEvent
      }
    }
  }
}

Then, the response can be filtered to only non-virtual POAPs and be formatted into an array of event IDs to be used in the next step:

const eventIds =
  data?.Poaps.Poap?.filter((poap) => !poap?.poapEvent?.isVirtualEvent).map(
    (poap) => poap?.eventId
  ) ?? [];

where data is the response from the API. The formatted result, will be an array of event IDs of the non-virtual POAPs owned by vitalik.eth:

[
  "80393",
  "79011",
  "15678",
  "76134",
  "149333"
  // other non-virtual POAP event IDs held by vitalik.eth
]

Fetch all POAP holders of an array of POAP event IDs

Using the array of event IDs from the first step, you can fetch all POAP holders that hold any of the POAPs that the given user, e.g. vitalik.eth, owned/attended:

Try Demo

show me POAP holders of an array of POAP event IDs

Code

query MyQuery($eventIds: [String!]) {
  Poaps(input: { filter: { eventId: { _in: $eventIds } }, blockchain: ALL }) {
    Poap {
      eventId
      poapEvent {
        eventName
        contentValue {
          image {
            extraSmall
          }
        }
      }
      attendee {
        owner {
          addresses
          domains {
            name
            isPrimary
          }
          socials {
            dappName
            blockchain
            profileName
            profileImage
            profileTokenId
            profileTokenAddress
          }
          xmtp {
            isXMTPEnabled
          }
        }
      }
    }
  }
}

The response then can be formatted further with the following formatting function to extract all the recommended users that has common POAPs with the given user:

utils/formatPoapsData.js
function formatPoapsData(poaps, exitingUser = []) {
  const recommendedUsers = [...exitingUser];
  for (const poap of poaps ?? []) {
    const { attendee, poapEvent, eventId } = poap ?? {};
    const { eventName: name, contentValue } = poapEvent ?? {};
    const { addresses } = attendee?.owner ?? {};
    const existingUserIndex = recommendedUsers.findIndex(
      ({ addresses: recommendedUsersAddresses }) =>
        recommendedUsersAddresses?.some?.((address) =>
          addresses?.includes?.(address)
        )
    );
    if (existingUserIndex !== -1) {
      recommendedUsers[existingUserIndex].addresses = [
        ...(recommendedUsers?.[existingUserIndex]?.addresses ?? []),
        ...addresses,
      ]?.filter((address, index, array) => array.indexOf(address) === index);
      const _poaps = recommendedUsers?.[existingUserIndex]?.poaps || [];
      const poapExists = _poaps.some((poap) => poap.eventId === eventId);
      if (!poapExists) {
        _poaps?.push({ name, image: contentValue?.image?.extraSmall, eventId });
        recommendedUsers[existingUserIndex].poaps = [..._poaps];
      }
    } else {
      recommendedUsers.push({
        ...(attendee?.owner ?? {}),
        poaps: [{ name, image: contentValue?.image?.extraSmall, eventId }],
      });
    }
  }
  return recommendedUsers;
}

export default formatPoapsData;

The formatted result will have a format as follows:

[
  {
    "addresses": ["0xd35f7c2f23fdc341aa8c7534f0e521679206a036"],
    "domains": [ { "name": "taoliu.eth", "isPrimary": true } ],
    "socials": [
      {
        "dappName": "lens",
        "blockchain": "polygon",
        "profileName": "lens/@colinlt",
        "profileImage": "",
        "profileTokenId": "33481",
        "profileTokenAddress": "0xdb46d1dc155634fbc732f92e853b10b288ad5a1d"
      }
    ],
    "xmtp": null,
    "poaps": [ // show all common POAPs also owned by vitalik.eth
      {
        "name": "Rocket Pool Bot Catcher POAP",
        "image": undefined,
        "eventId": "7426"
      }
    ]
  },
  // other onchain graph users
]

Iterate to fetch all common POAP holders data

With the queries for fetching common POAP holders established, it will be essential to fetch all the data using paginations.

In order to paginate through all the data, you can utilize fetchQueryWithPagination and execute_paginated_query from the JavaScript (React & Node) and Python SDKs, respectively. The full code implementation for this will be as follows:

functions/fetchPoapsData.js
import { init, fetchQueryWithPagination } from "@airstack/node"; // or @airstack/airstack-react for frontend javascript
import formatPoapsData from "../utils/formatPoapsData";

// get your API key at https://app.airstack.xyz/profile-settings/api-keys
init("YOUR_AIRSTACK_API_KEY");

const userPoapsEventIdsQuery = `
query MyQuery {
  Poaps(input: {filter: {owner: {_eq: "vitalik.eth"}}, blockchain: ALL}) {
    Poap {
      eventId
      poapEvent {
        isVirtualEvent
      }
    }
  }
}
`;

const poapsByEventIdsQuery = `
query MyQuery($eventIds: [String!]) {
  Poaps(input: {filter: {eventId: {_in: $eventIds}}, blockchain: ALL}) {
    Poap {
      eventId
      poapEvent {
        eventName
        contentValue {
          image {
            extraSmall
          }
        }
      }
      attendee {
        owner {
          addresses
          domains {
            name
            isPrimary
          }
          socials {
            dappName
            blockchain
            profileName
            profileImage
            profileTokenId
            profileTokenAddress
          }
          xmtp {
            isXMTPEnabled
          }
        }
      }
    }
  }
}
`;

const fetchPoapsData = async (address, existingUsers = []) => {
  let poapsDataResponse;
  let recommendedUsers = [...existingUsers];
  while (true) {
    if (!poapsDataResponse) {
      // Paagination #1: Fetch All POAPs
      poapsDataResponse = await fetchQueryWithPagination(
        userPoapsEventIdsQuery,
        {
          user: address,
        }
      );
    }
    const {
      data: poapsData,
      error: poapsError,
      hasNextPage: poapsHasNextPage,
      getNextPage: poapsGetNextPage,
    } = poapsDataResponse ?? {};
    if (!poapsError) {
      const eventIds =
        poapsData?.Poaps.Poap?.filter(
          (poap) => !poap?.poapEvent?.isVirtualEvent
        ).map((poap) => poap?.eventId) ?? [];
      let poapHoldersDataResponse;
      while (true) {
        if (eventIds.length === 0) break;
        if (!poapHoldersDataResponse) {
          // Pagination #2: Fetch All POAP holders
          poapHoldersDataResponse = await fetchQueryWithPagination(
            poapsByEventIdsQuery,
            {
              eventIds,
            }
          );
        }
        const {
          data: poapHoldersData,
          error: poapHoldersError,
          hasNextPage: poapHoldersHasNextPage,
          getNextPage: poapHoldersGetNextPage,
        } = poapHoldersDataResponse;
        if (!poapHoldersError) {
          recommendedUsers = [
            ...formatPoapsData(poapHoldersData?.Poaps?.Poap, recommendedUsers),
          ];
          if (!poapHoldersHasNextPage) {
            break;
          } else {
            poapHoldersDataResponse = await poapHoldersGetNextPage();
          }
        } else {
          console.error("Error: ", poapHoldersError);
          break;
        }
      }
      if (!poapsHasNextPage) {
        break;
      } else {
        poapsDataResponse = await poapsGetNextPage();
      }
    } else {
      console.error("Error: ", poapsError);
      break;
    }
  }
  return recommendedUsers;
};

export default fetchPoapsData;

Step 1.2: Fetch Farcaster Followings Data

You can use Airstack to easily fetch all the users that is being followed on Farcaster by a given user, e.g. vitalik.eth, and get their 0x addresses, ENS domains, Lens, Farcaster, and XMTP:

Try Demo

Show all Farcaster followings of vitalik.eth and check if they're mutual followings

Code

query MyQuery($user: Identity!) {
  SocialFollowings(
    input: {
      filter: { identity: { _eq: $user }, dappName: { _eq: farcaster } }
      blockchain: ALL
      limit: 200
    }
  ) {
    Following {
      followingAddress {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
        mutualFollower: socialFollowers(
          input: {
            filter: { identity: { _eq: $user }, dappName: { _eq: farcaster } }
          }
        ) {
          Follower {
            followerAddress {
              socials {
                profileName
              }
            }
          }
        }
      }
    }
  }
}

The response then can be formatted further with the following formatting function to extract all the recommended users that is being followed on Farcaster by the given user:

utils/formatFarcasterFollowingsData.js
function formatFarcasterFollowingsData(followings, existingUser = []) {
  const recommendedUsers = [...existingUser];
  for (const following of followings) {
    const existingUserIndex = recommendedUsers.findIndex(
      ({ addresses: recommendedUsersAddresses }) =>
        recommendedUsersAddresses?.some?.((address) =>
          following.addresses?.includes?.(address)
        )
    );

    const followsBack = Boolean(following?.mutualFollower?.Follower?.[0]);
    if (existingUserIndex !== -1) {
      const follows = recommendedUsers?.[existingUserIndex]?.follows ?? {};
      recommendedUsers[existingUserIndex] = {
        ...following,
        ...recommendedUsers[existingUserIndex],
        follows: {
          ...follows,
          followingOnFarcaster: true,
          followedOnFarcaster: followsBack,
        },
      };
    } else {
      recommendedUsers.push({
        ...following,
        follows: {
          followingOnFarcaster: true,
          followedOnFarcaster: followsBack,
        },
      });
    }
  }
  return recommendedUsers;
}

export default formatFarcasterFollowingsData;

The formatted result will have a format as follows:

[
  {
    "addresses": [
      "0xf6fd7deec77d7b1061435585df1d7fdfd4682577",
      "0x925afeb19355e289ed1346ede709633ca8788b25",
      "0x18b7511938fbe2ee08adf3d4a24edb00a5c9b783"
    ],
    "domains": [
      { "name": "phil.brightmoments.eth", "isPrimary": true },
      { "name": "centerforonchainstudies.eth", "isPrimary": false },
      { "name": "purple.philm.eth", "isPrimary": true },
      { "name": "centerforonchainstructure.eth", "isPrimary": false },
      { "name": "lorensbags.eth", "isPrimary": false },
      { "name": "onchainstudies.eth", "isPrimary": false }
    ],
    "socials": [
      {
        "dappName": "farcaster",
        "blockchain": "optimism",
        "profileName": "phil",
        "profileImage": "https://i.imgur.com/sx6qqM7.jpg",
        "profileTokenId": "129",
        "profileTokenAddress": "0x00000000fcaf86937e41ba038b4fa40baa4b780a"
      }
    ],
    "xmtp": [{ "isXMTPEnabled": true }],
    "mutualFollower": { "Follower": null },
    // show vitalik.eth following this user on Faracster and being followed back
    "follows": { "followingOnFarcaster": true, "followedOnFarcaster": true }
  },
  // other onchain graph users
]

Iterate to fetch all users being followed on Farcaster

With the queries for fetching all the users being followed on Farcaster of a given user established, it will be essential to fetch all the data using paginations.

In order to paginate through all the data, you can utilize fetchQueryWithPagination and execute_paginated_query from the JavaScript (React & Node) and Python SDKs, respectively. The full code implementation for this will be as follows:

functions/fetchFarcasterFollowings.js
import { init, fetchQueryWithPagination } from "@airstack/node"; // or @airstack/airstack-react for frontend javascript
import formatFarcasterFollowingsData from "../utils/formatFarcasterFollowingsData";

// get your API key at https://app.airstack.xyz/profile-settings/api-keys
init("YOUR_AIRSTACK_API_KEY");

const socialFollowingsQuery = `
query MyQuery($user: Identity!) {
  SocialFollowings(
    input: {filter: {identity: {_eq: $user}, dappName: {_eq: farcaster}}, blockchain: ALL, limit: 200}
  ) {
    Following {
      followingAddress {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
        mutualFollower: socialFollowers(
          input: {filter: {identity: {_eq: $user}, dappName: {_eq: farcaster}}}
        ) {
          Follower {
            followerAddress {
              socials {
                profileName
              }
            }
          }
        }
      }
    }
  }
}
`;

const fetchFarcasterFollowings = async (address, existingUsers = []) => {
  let farcasterFollowingsDataResponse;
  let recommendedUsers = [...existingUsers];
  while (true) {
    if (!farcasterFollowingsDataResponse) {
      farcasterFollowingsDataResponse = await fetchQueryWithPagination(
        socialFollowingsQuery,
        {
          user: address,
        }
      );
    }
    const {
      data: farcasterFollowingsData,
      error: farcasterFollowingsError,
      hasNextPage: farcasterFollowingsHasNextPage,
      getNextPage: farcasterFollowingsGetNextPage,
    } = farcasterFollowingsDataResponse ?? {};
    if (!farcasterFollowingsError) {
      const followings =
        farcasterFollowingsData?.SocialFollowings?.Following?.map(
          following => following.followingAddress
        ) ?? [];
      recommendedUsers = [
        ...formatFarcasterFollowingsData(followings, recommendedUsers),
      ];
      if (!farcasterFollowingsHasNextPage) {
        break;
      } else {
        farcasterFollowingsDataResponse = await farcasterFollowingsGetNextPage();
      }
    } else {
      console.error("Error: ", farcasterFollowingsError);
      break;
    }
  }
  return recommendedUsers;
};

export default fetchFarcasterFollowings;

Step 1.3: Fetch Lens Followings Data

You can use Airstack to easily fetch all the users that is being followed on Lens by a given user, e.g. vitalik.eth, and get their 0x addresses, ENS domains, Lens, Farcaster, and XMTP:

Try Demo

Show all Lens followings of vitalik.eth and check if they're mutual followings

Code

query MyQuery($user: Identity!) {
  SocialFollowings(
    input: {
      filter: { identity: { _eq: $user }, dappName: { _eq: lens } }
      blockchain: ALL
      limit: 200
    }
  ) {
    Following {
      followingAddress {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
        mutualFollower: socialFollowers(
          input: {
            filter: { identity: { _eq: $user }, dappName: { _eq: lens } }
          }
        ) {
          Follower {
            followerAddress {
              socials {
                profileName
              }
            }
          }
        }
      }
    }
  }
}

The response then can be formatted further with the following formatting function to extract all the recommended users that is being followed on Lens by the given user:

utils/formatLensFollowingsData.js
function formatLensFollowingsData(followings, existingUser = []) {
  const recommendedUsers = [...existingUser];
  for (const following of followings) {
    const existingUserIndex = recommendedUsers.findIndex(
      ({ addresses: recommendedUsersAddresses }) =>
        recommendedUsersAddresses?.some?.((address) =>
          following.addresses?.includes?.(address)
        )
    );

    const followsBack = Boolean(following?.mutualFollower?.Follower?.[0]);
    if (existingUserIndex !== -1) {
      const follows = recommendedUsers?.[existingUserIndex]?.follows ?? {};
      recommendedUsers[existingUserIndex] = {
        ...following,
        ...recommendedUsers[existingUserIndex],
        follows: {
          ...follows,
          followingOnLens: true,
          followedOnLens: followsBack,
        },
      };
    } else {
      recommendedUsers.push({
        ...following,
        follows: {
          followingOnLens: true,
          followedOnLens: followsBack,
        },
      });
    }
  }
  return recommendedUsers;
}

export default formatLensFollowingsData;

The formatted result will have a format as follows:

[
  {
    "addresses": ["0x648aa14e4424e0825a5ce739c8c68610e143fb79"],
    "domains": [
      { "name": "sassal.isstackingsats.eth", "isPrimary": false },
      {
        "name": "[2b95ffa321895f770d6cf4f5a0a28b503775a5791b879f7fd7dc8be4d2119539].ethmojis.eth",
        "isPrimary": false
      },
      { "name": "thedailygwei.eth", "isPrimary": false },
      {
        "name": "[77a1a3bec9ef5f3ae2bb016067fb690ea5db004f01c5628c015b15c0c2954b1c].ethmojis.eth",
        "isPrimary": false
      },
      {
        "name": "[799a91224d75d9f60ff17c9704dff211ac00d58d9c0f929f9bd0c972dc1d9e1b].ethmojis.eth",
        "isPrimary": false
      },
      { "name": "sassal.eth", "isPrimary": true },
      {
        "name": "[61a23a96d60aa46f53bd6ea7db88aa1ccca962bf18d4f3fbeef46aa611783032].ethmojis.eth",
        "isPrimary": false
      },
      { "name": "thedailygwei.mirror.xyz", "isPrimary": false },
      { "name": "sassal.ismoney.eth", "isPrimary": false },
      { "name": "sassal.defiโšก.eth", "isPrimary": false }
    ],
    "socials": [
      {
        "dappName": "farcaster",
        "blockchain": "optimism",
        "profileName": "",
        "profileImage": "",
        "profileTokenId": "21566",
        "profileTokenAddress": "0x00000000fcaf86937e41ba038b4fa40baa4b780a"
      },
      {
        "dappName": "lens",
        "blockchain": "polygon",
        "profileName": "lens/@sassal",
        "profileImage": "",
        "profileTokenId": "13464",
        "profileTokenAddress": "0xdb46d1dc155634fbc732f92e853b10b288ad5a1d"
      },
      {
        "dappName": "farcaster",
        "blockchain": "optimism",
        "profileName": "sassal.eth",
        "profileImage": "https://i.imgur.com/J81m7He.jpg",
        "profileTokenId": "4036",
        "profileTokenAddress": "0x00000000fcaf86937e41ba038b4fa40baa4b780a"
      }
    ],
    "xmtp": null,
    "mutualFollower": { "Follower": null },
    // shows vitalik.eth following this user, but not followed back on Lens
    "follows": { "followingOnLens": true, "followedOnLens": false }
  }
]

Iterate to fetch all users being followed on Lens

With the queries for fetching all the users being followed on Lens of a given user established, it will be essential to fetch all the data using paginations.

In order to paginate through all the data, you can utilize fetchQueryWithPagination and execute_paginated_query from the JavaScript (React & Node) and Python SDKs, respectively. The full code implementation for this will be as follows:

functions/fetchLensFollowings.js
import { init, fetchQueryWithPagination } from "@airstack/node"; // or @airstack/airstack-react for frontend javascript
import formatLensFollowingsData from "../utils/formatLensFollowingsData";

// get your API key at https://app.airstack.xyz/profile-settings/api-keys
init("YOUR_AIRSTACK_API_KEY");

const socialFollowingsQuery = `
query MyQuery($user: Identity!) {
    SocialFollowings(
      input: {filter: {identity: {_eq: $user}, dappName: {_eq: lens}}, blockchain: ALL, limit: 200}
    ) {
      Following {
        followingAddress {
          addresses
          domains {
            name
            isPrimary
          }
          socials {
            dappName
            blockchain
            profileName
            profileImage
            profileTokenId
            profileTokenAddress
          }
          xmtp {
            isXMTPEnabled
          }
          mutualFollower: socialFollowers(
            input: {filter: {identity: {_eq: $user}, dappName: {_eq: lens}}}
          ) {
            Follower {
              followerAddress {
                socials {
                  profileName
                }
              }
            }
          }
        }
      }
    }
  }
`;

const fetchLensFollowings = async (address, existingUsers = []) => {
  let res;
  let recommendedUsers = [...existingUsers];
  while (true) {
    if (!res) {
      res = await fetchQueryWithPagination(socialFollowingsQuery, {
        user: address,
      });
    }
    const { data, error, hasNextPage, getNextPage } = res ?? {};
    if (!error) {
      const followings =
        data?.SocialFollowings?.Following?.map(
          (following) => following.followingAddress
        ) ?? [];
      recommendedUsers = [
        ...formatLensFollowingsData(followings, recommendedUsers),
      ];
      if (!hasNextPage) {
        break;
      } else {
        res = await getNextPage();
      }
    } else {
      console.error("Error: ", error);
      break;
    }
  }
  return recommendedUsers;
};

export default fetchLensFollowings;

Step 1.4: Fetch Farcaster Followers Data

You can use Airstack to easily fetch all the users that is following a given user, e.g. vitalik.eth, on Farcaster and get their 0x addresses, ENS domains, Lens, Farcaster, and XMTP:

Try Demo

Code

query MyQuery($user: Identity!) {
  SocialFollowers(
    input: {
      filter: { identity: { _eq: $user }, dappName: { _eq: farcaster } }
      blockchain: ALL
      limit: 200
    }
  ) {
    Follower {
      followerAddress {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
        mutualFollowing: socialFollowings(
          input: {
            filter: { identity: { _eq: $user }, dappName: { _eq: farcaster } }
          }
        ) {
          Following {
            followingAddress {
              socials {
                profileName
              }
            }
          }
        }
      }
    }
  }
}

The response then can be formatted further with the following formatting function to extract all the recommended users that is following a given user on Farcaster:

utils/formatFarcasterFollowersData.js
function formatFarcasterFollowersData(followers, existingUser = []) {
  const recommendedUsers = [...existingUser];

  for (const follower of followers) {
    const existingUserIndex = recommendedUsers.findIndex(
      ({ addresses: recommendedUsersAddresses }) =>
        recommendedUsersAddresses?.some?.((address) =>
          follower.addresses?.includes?.(address)
        )
    );

    const following = Boolean(follower?.mutualFollowing?.Following?.length);

    if (existingUserIndex !== -1) {
      const follows = recommendedUsers?.[existingUserIndex]?.follows ?? {};

      follows.followedOnFarcaster = true;
      follows.followingOnFarcaster = follows.followingOnFarcaster || following;

      recommendedUsers[existingUserIndex] = {
        ...follower,
        ...recommendedUsers[existingUserIndex],
        follows,
      };
    } else {
      recommendedUsers.push({
        ...follower,
        follows: {
          followingOnFarcaster: following,
          followedOnFarcaster: true,
        },
      });
    }
  }
  return recommendedUsers;
}

export default formatFarcasterFollowersData;

The formatted result will have a format as follows:

[
  {
    "addresses": ["0xca9af28a4e0a73a74fd481a00d1145130f17586d"],
    "domains": null,
    "socials": [
      {
        "dappName": "farcaster",
        "blockchain": "optimism",
        "profileName": "thessy",
        "profileImage": "https://i.imgur.com/7xIpCuE.jpg",
        "profileTokenId": "5910",
        "profileTokenAddress": "0x00000000fcaf86937e41ba038b4fa40baa4b780a"
      }
    ],
    "xmtp": null,
    "mutualFollowing": { "Following": null },
    // show vitalik.eth being followed by this user, but not following back on Farcaster
    "follows": { "followingOnFarcaster": false, "followedOnFarcaster": true }
  },
  // other onchain graph users
]

Iterate to fetch all users following on Farcaster

With the queries for fetching all the users following on Farcaster of a given user established, it will be essential to fetch all the data using paginations.

In order to paginate through all the data, you can utilize fetchQueryWithPagination and execute_paginated_query from the JavaScript (React & Node) and Python SDKs, respectively. The full code implementation for this will be as follows:

functions/fetchFarcasterFollowers.js
import { init, fetchQueryWithPagination } from "@airstack/node"; // or @airstack/airstack-react for frontend javascript
import formatFarcasterFollowersData from "./utils/formatFarcasterFollowersData";

// get your API key at https://app.airstack.xyz/profile-settings/api-keys
init("YOUR_AIRSTACK_API_KEY");

const socialFollowersQuery = `
query MyQuery($user: Identity!) {
  SocialFollowers(
    input: {filter: {identity: {_eq: $user}, dappName: {_eq: farcaster}}, blockchain: ALL, limit: 200}
  ) {
    Follower {
      followerAddress {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
        mutualFollowing: socialFollowings(
          input: {filter: {identity: {_eq: $user}, dappName: {_eq: farcaster}}}
        ) {
          Following {
            followingAddress {
              socials {
                profileName
              }
            }
          }
        }
      }
    }
  }
}
`;

const fetchFarcasterFollowers = async (address, existingUsers = []) => {
  let res;
  let recommendedUsers = [...existingUsers];
  while (true) {
    if (!res) {
      res = await fetchQueryWithPagination(socialFollowersQuery, {
        user: address,
      });
    }
    const { data, error, hasNextPage, getNextPage } = res ?? {};
    if (!error) {
      const followings =
        data?.SocialFollowers?.Follower?.map(
          (follower) => follower.followerAddress
        ) ?? [];
      recommendedUsers = [
        ...formatFarcasterFollowersData(followings, recommendedUsers),
      ];
      if (!hasNextPage) {
        break;
      } else {
        res = await getNextPage();
      }
    } else {
      console.error("Error: ", error);
      break;
    }
  }
  return recommendedUsers;
};

export default fetchFarcasterFollowers;

Step 1.5: Fetch Lens Followers Data

You can use Airstack to easily fetch all the users that is following a given user, e.g. vitalik.eth, on Lens and get their 0x addresses, ENS domains, Lens, Farcaster, and XMTP:

Try Demo

Code

query MyQuery($user: Identity!) {
  SocialFollowers(
    input: {
      filter: { identity: { _eq: $user }, dappName: { _eq: lens } }
      blockchain: ALL
      limit: 200
    }
  ) {
    Follower {
      followerAddress {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
        mutualFollowing: socialFollowings(
          input: {
            filter: { identity: { _eq: $user }, dappName: { _eq: lens } }
          }
        ) {
          Following {
            followingAddress {
              socials {
                profileName
              }
            }
          }
        }
      }
    }
  }
}

The response then can be formatted further with the following formatting function to extract all the recommended users that is following a given user on Lens:

utils/formatLensFollowersData.js
function formatLensFollowersData(followers, existingUser = []) {
  const recommendedUsers = [...existingUser];

  for (const follower of followers) {
    const existingUserIndex = recommendedUsers.findIndex(
      ({ addresses: recommendedUsersAddresses }) =>
        recommendedUsersAddresses?.some?.((address) =>
          follower.addresses?.includes?.(address)
        )
    );

    const following = Boolean(follower?.mutualFollower?.Following?.length);

    if (existingUserIndex !== -1) {
      const follows = recommendedUsers?.[existingUserIndex]?.follows ?? {};

      follows.followedOnLens = true;
      follows.followingOnLens = follows.followingOnLens || following;

      recommendedUsers[existingUserIndex] = {
        ...follower,
        ...recommendedUsers[existingUserIndex],
        follows,
      };
    } else {
      recommendedUsers.push({
        ...follower,
        follows: {
          followingOnLens: following,
          followedOnLens: true,
        },
      });
    }
  }
  return recommendedUsers;
}

export default formatLensFollowersData;

The formatted result will have a format as follows:

[
  {
    "addresses": ["0x55b0c2662fe08f265e658ce235151b689d5e120c"],
    "domains": null,
    "socials": [
      {
        "dappName": "lens",
        "blockchain": "polygon",
        "profileName": "lens/@huxley_warner",
        "profileImage": "",
        "profileTokenId": "8818",
        "profileTokenAddress": "0xdb46d1dc155634fbc732f92e853b10b288ad5a1d"
      }
    ],
    "xmtp": [{ "isXMTPEnabled": true }],
    "mutualFollowing": { "Following": null },
    // show vitalik.eth followed by this user on Lens, but not following back
    "follows": { "followingOnLens": false, "followedOnLens": true }
  },
  // more onchain graph users
]

Iterate to fetch all users following on Lens

With the queries for fetching all the users following on Lens of a given user established, it will be essential to fetch all the data using paginations.

In order to paginate through all the data, you can utilize fetchQueryWithPagination and execute_paginated_query from the JavaScript (React & Node) and Python SDKs, respectively. The full code implementation for this will be as follows:

function/fetchLensFollowers.js
import { init, fetchQueryWithPagination } from "@airstack/node"; // or @airstack/airstack-react for frontend javascript
import formatLensFollowersData from "./utils/formatLensFollowersData";

// get your API key at https://app.airstack.xyz/profile-settings/api-keys
init("YOUR_AIRSTACK_API_KEY");

const socialFollowingsQuery = `
query MyQuery($user: Identity!) {
  SocialFollowers(
    input: {filter: {identity: {_eq: $user}, dappName: {_eq: lens}}, blockchain: ALL, limit: 200}
  ) {
    Follower {
      followerAddress {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
        mutualFollowing: socialFollowings(
          input: {filter: {identity: {_eq: $user}, dappName: {_eq: lens}}}
        ) {
          Following {
            followingAddress {
              socials {
                profileName
              }
            }
          }
        }
      }
    }
  }
}
`;

const fetchLensFollowers = async (address, existingUsers = []) => {
  let res;
  let recommendedUsers = [...existingUsers];
  while (true) {
    if (!res) {
      res = await fetchQueryWithPagination(socialFollowingsQuery, {
        user: address,
      });
    }
    const { data, error, hasNextPage, getNextPage } = res ?? {};
    if (!error) {
      const followings =
        data?.SocialFollowers?.Follower?.map(
          (follower) => follower.followerAddress
        ) ?? [];
      recommendedUsers = [
        ...formatLensFollowersData(followings, recommendedUsers),
      ];
      if (!hasNextPage) {
        break;
      } else {
        res = await getNextPage();
      }
    } else {
      console.error("Error: ", error);
      break;
    }
  }
  return recommendedUsers;
};

export default fetchLensFollowers;

Step 1.6: Fetch Token Transfers Sent Data

You can use Airstack to easily fetch all the users that received token transfers sent from a given user, e.g. vitalik.eth, and get their 0x addresses, ENS domains, Lens, Farcaster, and XMTP:

Try Demo

Code

query MyQuery($user: Identity!) {
  Ethereum: TokenTransfers(
    input: {
      filter: { from: { _eq: $user } }
      blockchain: ethereum
      limit: 200
    }
  ) {
    TokenTransfer {
      account: to {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
  Base: TokenTransfers(
    input: { filter: { from: { _eq: $user } }, blockchain: base, limit: 200 }
  ) {
    TokenTransfer {
      account: to {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
  Zora: TokenTransfers(
    input: { filter: { from: { _eq: $user } }, blockchain: base, limit: 200 }
  ) {
    TokenTransfer {
      account: to {
        addresses
        primaryDomain {
          name
        }
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
}

The response then can be formatted further with the following formatting function to extract all the recommended users that received token transfers sent from a given user:

utils/formatTokenSentData.js
const formatTokenSentData = (data, _recommendedUsers = []) => {
  const recommendedUsers = [..._recommendedUsers];

  for (const transfer of data) {
    const { addresses = [] } = transfer || {};
    const existingUserIndex = recommendedUsers.findIndex(
      ({ addresses: recommendedUsersAddresses }) =>
        recommendedUsersAddresses?.some?.((address) =>
          addresses?.includes?.(address)
        )
    );
    const _tokenTransfers = {
      sent: true,
    };
    if (existingUserIndex !== -1) {
      const _addresses = recommendedUsers?.[existingUserIndex]?.addresses || [];
      recommendedUsers[existingUserIndex].addresses = [
        ..._addresses,
        ...addresses,
      ]?.filter((address, index, array) => array.indexOf(address) === index);
      recommendedUsers[existingUserIndex].tokenTransfers = {
        ...(recommendedUsers?.[existingUserIndex]?.tokenTransfers ?? {}),
        ..._tokenTransfers,
      };
    } else {
      recommendedUsers.push({
        ...transfer,
        tokenTransfers: {
          ..._tokenTransfers,
        },
      });
    }
  }

  return recommendedUsers;
};

export default formatTokenSentData;

The formatted result will have a format as follows:

{
  "addresses": ["0xd8b75eb7bd778ac0b3f5ffad69bcc2e25bccac95"],
  "primaryDomain": { "name": "toastmybread.eth" },
  "domains": [
    { "name": "toastmybread.eth", "isPrimary": true },
    { "name": "daerbymtsaot.eth", "isPrimary": false }
  ],
  "socials": null,
  "xmtp": null,
  // This user receive tokens sent by vitalik.eth
  "tokenTransfers": { "sent": true }
}

Iterate to fetch all users that received token transfers sent from a given user

With the queries for fetching all the users that received token transfers sent from a given user established, it will be essential to fetch all the data using paginations.

In order to paginate through all the data, you can utilize fetchQueryWithPagination and execute_paginated_query from the JavaScript (React & Node) and Python SDKs, respectively. The full code implementation for this will be as follows:

functions/fetchTokenSent.js
import { init, fetchQueryWithPagination } from "@airstack/node"; // or @airstack/airstack-react for frontend javascript
import formatTokenSentData from "./utils/formatTokenSentData";

// get your API key at https://app.airstack.xyz/profile-settings/api-keys
init("YOUR_AIRSTACK_API_KEY");

const tokenSentQuery = `
query TokenSent($user: Identity!) {
    Ethereum: TokenTransfers(
      input: {filter: {from: {_eq: $user}}, blockchain: ethereum, limit: 200}
    ) {
      TokenTransfer {
        account: to {
          addresses
          primaryDomain {
            name
          }
          domains {
            name
            isPrimary
          }
          socials {
            dappName
            blockchain
            profileName
            profileImage
            profileTokenId
            profileTokenAddress
          }
          xmtp {
            isXMTPEnabled
          }
        }
      }
    }
    Base: TokenTransfers(
      input: {filter: {from: {_eq: $user}}, blockchain: base, limit: 200}
    ) {
      TokenTransfer {
        account: to {
          addresses
          primaryDomain {
            name
          }
          domains {
            name
            isPrimary
          }
          socials {
            dappName
            blockchain
            profileName
            profileImage
            profileTokenId
            profileTokenAddress
          }
          xmtp {
            isXMTPEnabled
          }
        }
      }
    }
    Zora: TokenTransfers(
      input: {filter: {from: {_eq: $user}}, blockchain: zora, limit: 200}
    ) {
      TokenTransfer {
        account: to {
          addresses
          primaryDomain {
            name
          }
          domains {
            name
            isPrimary
          }
          socials {
            dappName
            blockchain
            profileName
            profileImage
            profileTokenId
            profileTokenAddress
          }
          xmtp {
            isXMTPEnabled
          }
        }
      }
    }
  }
`;

const fetchTokenSent = async (address, existingUsers = []) => {
  let res;
  let recommendedUsers = [...existingUsers];
  while (true) {
    if (!res) {
      res = await fetchQueryWithPagination(tokenSentQuery, {
        user: address,
      });
    }
    const { data, error, hasNextPage, getNextPage } = res ?? {};
    if (!error) {
      const ethData = (data?.Ethereum?.TokenTransfer ?? []).map(
        (transfer) => transfer.account
      );
      const baseData = (data?.Base?.TokenTransfer ?? []).map(
        (transfer) => transfer.account
      );
      const zoraData = (data?.Zora?.TokenTransfer ?? []).map(
        (transfer) => transfer.account
      );
      const tokenTransfer = [
        ...ethData,
        ...baseData,
        ...zoraData,
      ];
      recommendedUsers = [
        ...formatTokenSentData(tokenTransfer, recommendedUsers),
      ];
      if (!hasNextPage) {
        break;
      } else {
        res = await getNextPage();
      }
    } else {
      console.error("Error: ", error);
      break;
    }
  }
  return recommendedUsers;
};

export default fetchTokenSent;

Step 1.7: Fetch Token Transfers Received Data

You can use Airstack to easily fetch all the users that sent token transfers to and received by a given user, e.g. vitalik.eth, and get their 0x addresses, ENS domains, Lens, Farcaster, and XMTP:

Try Demo

Code

query MyQuery($user: Identity!) {
  Ethereum: TokenTransfers(
    input: { filter: { to: { _eq: $user } }, blockchain: ethereum, limit: 200 }
  ) {
    TokenTransfer {
      account: from {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
  Base: TokenTransfers(
    input: { filter: { to: { _eq: $user } }, blockchain: base, limit: 200 }
  ) {
    TokenTransfer {
      account: from {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
  Zora: TokenTransfers(
    input: { filter: { to: { _eq: $user } }, blockchain: zora, limit: 200 }
  ) {
    TokenTransfer {
      account: from {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
}

The response then can be formatted further with the following formatting function to extract all the recommended users that sent token transfers to a given user:

utils/formatTokenReceivedData.js
const formatTokenReceivedData = (data, _recommendedUsers = []) => {
  const recommendedUsers = [..._recommendedUsers];

  for (const transfer of data) {
    const { addresses = [] } = transfer || {};
    const existingUserIndex = recommendedUsers.findIndex(
      ({ addresses: recommendedUsersAddresses }) =>
        recommendedUsersAddresses?.some?.((address) =>
          addresses?.includes?.(address)
        )
    );
    const _tokenTransfers = {
      received: true,
    };
    if (existingUserIndex !== -1) {
      const _addresses = recommendedUsers?.[existingUserIndex]?.addresses || [];
      recommendedUsers[existingUserIndex].addresses = [
        ..._addresses,
        ...addresses,
      ]?.filter((address, index, array) => array.indexOf(address) === index);
      recommendedUsers[existingUserIndex].tokenTransfers = {
        ...(recommendedUsers?.[existingUserIndex]?.tokenTransfers ?? {}),
        ..._tokenTransfers,
      };
    } else {
      recommendedUsers.push({
        ...transfer,
        tokenTransfers: {
          ..._tokenTransfers,
        },
      });
    }
  }

  return recommendedUsers;
};

export default formatTokenReceivedData;

The formatted result will have a format as follows:

[
  {
    "addresses": ["0xd8da6bf26964af9d7eed9e03e53415d37aa96045"],
    "domains": [
      { "name": "quantumexchange.eth", "isPrimary": false },
      { "name": "7860000.eth", "isPrimary": false },
      { "name": "offchainexample.eth", "isPrimary": false },
      { "name": "brianshaw.eth", "isPrimary": false },
      { "name": "vbuterin.stateofus.eth", "isPrimary": false },
      { "name": "quantumsmartcontracts.eth", "isPrimary": false },
      { "name": "Vitalik.eth", "isPrimary": false },
      { "name": "openegp.eth", "isPrimary": false },
      { "name": "vitalik.cannafam.eth", "isPrimary": false },
      { "name": "VITALIK.eth", "isPrimary": false }
    ],
    "socials": [
      {
        "dappName": "farcaster",
        "blockchain": "optimism",
        "profileName": "vitalik.eth",
        "profileImage": "https://i.imgur.com/gF9Yaeg.jpg",
        "profileTokenId": "5650",
        "profileTokenAddress": "0x00000000fcaf86937e41ba038b4fa40baa4b780a"
      },
      {
        "dappName": "lens",
        "blockchain": "polygon",
        "profileName": "lens/@vitalik",
        "profileImage": "",
        "profileTokenId": "100275",
        "profileTokenAddress": "0xdb46d1dc155634fbc732f92e853b10b288ad5a1d"
      }
    ],
    "xmtp": [{ "isXMTPEnabled": true }],
    // show that vitalik.eth received token transfers sent by this user
    "tokenTransfers": { "received": true }
  },
  // other onchain graph users
]

Iterate to fetch all users that sent token transfers to a given user

With the queries for fetching all the users that sent token transfers to a given user established, it will be essential to fetch all the data using paginations.

In order to paginate through all the data, you can utilize fetchQueryWithPagination and execute_paginated_query from the JavaScript (React & Node) and Python SDKs, respectively. The full code implementation for this will be as follows:

functions/fetchTokenReceived.js
import { init, fetchQueryWithPagination } from "@airstack/node"; // or @airstack/airstack-react for frontend javascript
import formatTokenReceivedData from "./utils/formatTokenReceivedData";

// get your API key at https://app.airstack.xyz/profile-settings/api-keys
init("YOUR_AIRSTACK_API_KEY");

const tokenReceivedQuery = `
query MyQuery($user: Identity!) {
  Ethereum: TokenTransfers(
    input: {filter: {to: {_eq: $user}}, blockchain: ethereum, limit: 200}
  ) {
    TokenTransfer {
      account: from {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
  Base: TokenTransfers(
    input: {filter: {to: {_eq: $user}}, blockchain: base, limit: 200}
  ) {
    TokenTransfer {
      account: from {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
  Zora: TokenTransfers(
    input: {filter: {to: {_eq: $user}}, blockchain: zora, limit: 200}
  ) {
    TokenTransfer {
      account: from {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
}
`;

const fetchTokenReceived = async (address, existingUsers = []) => {
  let res;
  let recommendedUsers = [...existingUsers];
  while (true) {
    if (!res) {
      res = await fetchQueryWithPagination(tokenReceivedQuery, {
        user: address,
      });
    }
    const { data, error, hasNextPage, getNextPage } = res ?? {};
    if (!error) {
      const ethData = (data?.Ethereum?.TokenTransfer ?? []).map(
        (transfer) => transfer.account
      );
      const baseData = (data?.Base?.TokenTransfer ?? []).map(
        (transfer) => transfer.account
      );
      const zoraData = (data?.Zora?.TokenTransfer ?? []).map(
        (transfer) => transfer.account
      );

      const tokenTransfer = [
        ...ethData,
        ...baseData,
        ...zoraData
      ];
      recommendedUsers = [
        ...formatTokenReceivedData(tokenTransfer, recommendedUsers),
      ];
      if (!hasNextPage) {
        break;
      } else {
        res = await getNextPage();
      }
    } else {
      console.error("Error: ", error);
      break;
    }
  }
  return recommendedUsers;
};

export default fetchTokenReceived;

Step 1.8: Fetch Common Ethereum NFT Holders Data

In order to fetch the common Ethereum holders that hold the Ethereum NFTs hold by a given user, it will require 2 steps:

Fetch all Ethereum NFTs owned by a user

You can use Airstack to fetch all the NFTs that are hold by a given user, e.g. vitalik.eth, on Ethereum:

Try Demo

Code

query MyQuery($user: Identity!) {
  TokenBalances(
    input: {
      filter: { tokenType: { _in: [ERC721] }, owner: { _eq: $user } }
      blockchain: ethereum
      limit: 200
    }
  ) {
    TokenBalance {
      tokenAddress
    }
  }
}

Then, the response can be filtered to only Ethereum NFTs and be formatted into an array of token addresses to be used in the next step:

const tokenAddresses =
  data?.TokenBalances?.TokenBalance?.map((token) => token.tokenAddress) ?? [];

where data is the response from the API. The formatted result will have a format as follows:

[
  "0x2f1d6bba8d2ce2f1b6aed78f797096a6cc9e9fc9",
  "0x9251dec8df720c2adf3b6f46d968107cbbadf4d4",
  "0xcde7c3d9629f6bf247b4a4601260bd8fb7554ec6",
  "0x77e2545d1d63856e22ce82e3d6f2a3b2077232bf",
  "0xfbbddd98640eb24732f3c65a0348825055d2d651",
  "0x932261f9fc8da46c4a22e31b45c4de60623848bf",
  "0x160da290a6b1923257705cb05c322ae44ca86ebb",
  "0x0a1e7f376c586e272a3070632c8297c91b1d1b32",
  "0x343f999eaacdfa1f201fb8e43ebb35c99d9ae0c1",
  "0x9fa184c43b00da59b06f2296d509fbb465fb362e",
  "0xf18224ab6479bb1ecb908d4fe7d2c366d49df0fc",
  "0xb365e53b64655476e3c3b7a3e225d8bf2e95f71d",
  "0x7d89b4c0e85634f0587946b0c8370f477c645a80",
  "0x60f80121c31a0d46b5279700f9df786054aa5ee5",
  "0x765c1d9b32bb20c143aeebfe56e6e7f15d2e8af0",
  "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85",
  "0x8d0802559775c70fb505f22988a4fd4a4f6d3b62",
  "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85",
  "0x02e9b2389156ee8ed963b1341a69d5f54ada4d82",
  "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85",
  "0xb93ee8cdab36199c6debf5bbec53e5908fd8e4e1",
  "0x0a1bbd57033f57e7b6743621b79fcb9eb2ce3676",
  "0x044ec6ce7e87859eb9d3ca966cadfb7926d0c482",
  "0xd9e4f99ff4582c710686e30efff39776a055039b",
  "0x172750a992eeee819394dcbab0c86dab5f94b557"
  // other Ethereum NFT addresses
]

Fetch all Ethereum NFT owners

Using the array of token addresses from the first step, you can fetch all Ethereum NFT holders that hold any of the NFTs that the given user, e.g. vitalik.eth, owned on Ethereum:

Try Demo

Code

query MyQuery($tokenAddresses: [Address!]) {
  TokenBalances(
    input: {
      filter: {
        tokenAddress: { _in: $tokenAddresses }
        tokenType: { _in: [ERC721] }
      }
      blockchain: ethereum
      limit: 200
    }
  ) {
    TokenBalance {
      token {
        name
        address
        tokenNfts {
          tokenId
        }
        blockchain
        logo {
          small
        }
      }
      owner {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
}

The response then can be formatted further with the following formatting function to extract all the recommended users that has common Ethereum NFTs with the given user:

utils/formatEthNftData.js
const formatEthNftData = (data, _recommendedUsers = []) => {
  const recommendedUsers = [..._recommendedUsers];

  for (const nft of data) {
    const { owner, token } = nft ?? {};
    const { name, logo, address, tokenNfts = [] } = token ?? {};
    const { addresses } = owner ?? {};
    const tokenNft = tokenNfts?.[0];

    const existingUserIndex = recommendedUsers.findIndex(
      ({ addresses: recommendedUsersAddresses }) =>
        recommendedUsersAddresses?.some?.((address) =>
          addresses?.includes?.(address)
        )
    );

    if (existingUserIndex !== -1) {
      const _addresses = recommendedUsers?.[existingUserIndex]?.addresses || [];
      recommendedUsers[existingUserIndex].addresses = [
        ..._addresses,
        ...addresses,
      ]?.filter((address, index, array) => array.indexOf(address) === index);
      const _nfts = recommendedUsers?.[existingUserIndex]?.nfts || [];
      const nftExists = _nfts.some((nft) => nft.address === address);
      if (!nftExists) {
        _nfts?.push({
          name,
          image: logo?.small,
          blockchain: "ethereum",
          address,
          tokenNfts: tokenNft,
        });
      }
      recommendedUsers[existingUserIndex].nfts = [..._nfts];
    } else {
      recommendedUsers.push({
        ...owner,
        nfts: [
          {
            name,
            image: logo?.small,
            blockchain: "ethereum",
            address,
            tokenNfts: tokenNft,
          },
        ],
      });
    }
  }
  return recommendedUsers;
};

export default formatEthNftData;

The formatted result will have a format as follows:

[
  {
    "addresses": ["0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401"],
    "domains": [
      { "name": "namewrapper.eth", "isPrimary": false },
      { "name": "๐Ÿธ.lovespepe.eth", "isPrimary": false },
      { "name": "wrapper.ens.eth", "isPrimary": true }
    ],
    "socials": null,
    "xmtp": null,
    // show all common Ethereum NFTs that is also owned by vitalik.eth
    "nfts": [
      {
        "name": "Ethereum Name Service",
        "image": "https://assets.airstack.xyz/image/logo/BQrUBoUPz7YtP+f8AdOKeXhU5q9k47EfLHh6VHoZnGcoMtWouWirBO3gxIG42YCJ/small.png",
        "blockchain": "ethereum",
        "address": "0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85",
        "tokenNfts": { "tokenId": "0" }
      },
      // other NFTs
    ]
  },
  // other onchain graph users
]

Iterate to fetch all common Ethereum NFT holders

With the queries for fetching all the common Ethereum NFT holders that holds the same Ethereum NFTs as the given user established, it will be essential to fetch all the data using paginations.

In order to paginate through all the data, you can utilize fetchQueryWithPagination and execute_paginated_query from the JavaScript (React & Node) and Python SDKs, respectively. The full code implementation for this will be as follows:

functions/fetchEthNft.js
import { init, fetchQueryWithPagination } from "@airstack/node"; // or @airstack/airstack-react for frontend javascript
import formatEthNftData from "./utils/formatEthNftData";

// get your API key at https://app.airstack.xyz/profile-settings/api-keys
init("YOUR_AIRSTACK_API_KEY");

const nftAddressesQuery = `
query MyQuery($user: Identity!) {
  TokenBalances(input: {filter: {tokenType: {_in: [ERC721]}, owner: {_eq: $user}}, blockchain: ethereum, limit: 200}) {
    TokenBalance {
      tokenAddress
    }
  }
}
`;

const nftQuery = `
query MyQuery($tokenAddresses: [Address!]) {
  TokenBalances(
    input: {filter: {tokenAddress: {_in: $tokenAddresses}, tokenType: {_in: [ERC721]}}, blockchain: ethereum, limit: 200}
  ) {
    TokenBalance {
      token {
        name
        address
        tokenNfts {
          tokenId
        }
        blockchain
        logo {
          small
        }
      }
      owner {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
}
`;

const fetchEthNft = async (address, existingUsers = []) => {
  let ethNftDataResponse;
  let recommendedUsers = [...existingUsers];
  while (true) {
    if (!ethNftDataResponse) {
      // Pagination #1: Fetch Ethereum NFTs
      ethNftDataResponse = await fetchQueryWithPagination(nftAddressesQuery, {
        user: address,
      });
    }
    const {
      data: ethNftData,
      error: ethNftError,
      hasNextPage: ethNftHasNextPage,
      getNextPage: ethNftGetNextPage,
    } = ethNftDataResponse ?? {};
    if (!ethNftError) {
      const tokenAddresses =
        ethNftData?.TokenBalances?.TokenBalance?.map(
          (token) => token.tokenAddress
        ) ?? [];
      let ethNftHoldersDataResponse;
      while (true) {
        if (tokenAddresses.length === 0) break;
        if (!ethNftHoldersDataResponse) {
          // Pagination #2: Fetch Ethereum NFT Holders
          ethNftHoldersDataResponse = await fetchQueryWithPagination(nftQuery, {
            tokenAddresses,
          });
        }
        const {
          data: ethNftHoldersData,
          error: ethNftHoldersError,
          hasNextPage: ethNftHoldersHasNextPage,
          getNextPage: ethNftHoldersGetNextPage,
        } = ethNftHoldersDataResponse;
        if (!ethNftHoldersError) {
          recommendedUsers = [
            ...formatEthNftData(
              ethNftHoldersData?.TokenBalances?.TokenBalance,
              recommendedUsers
            ),
          ];
          if (!ethNftHoldersHasNextPage) {
            break;
          } else {
            ethNftHoldersDataResponse = await ethNftHoldersGetNextPage();
          }
        } else {
          console.error("Error: ", ethNftHoldersError);
          break;
        }
      }
      if (!ethNftHasNextPage) {
        break;
      } else {
        ethNftDataResponse = await ethNftGetNextPage();
      }
    } else {
      console.error("Error: ", ethNftError);
      break;
    }
  }
  return recommendedUsers;
};

export default fetchEthNft;

Step 1.9: Fetch Common Base NFT Holders Data

You can use Airstack to fetch all the NFTs that are hold by a given user, e.g. vitalik.eth, on Base:

Try Demo

Code

query MyQuery($user: Identity!) {
  TokenBalances(
    input: {
      filter: { tokenType: { _in: [ERC721] }, owner: { _eq: $user } }
      blockchain: base
      limit: 200
    }
  ) {
    TokenBalance {
      tokenAddress
    }
  }
}

Then, the response can be filtered to only Base NFTs and be formatted into an array of token addresses to be used in the next step:

const tokenAddresses =
  data?.TokenBalances?.TokenBalance?.map((token) => token.tokenAddress) ?? [];

where data is the response from the API. The formatted result will have a format as follows:

[
  "0x273db54929d8392c1997be361da89d41af202a49",
  "0x3325c30baf2c97a7b8f28d4418e803104ad9e5b9",
  "0x344bd884b47dfc988f7e47851d576e0ac083a16f",
  "0x1c6fbcf5a97c2e95af33086aad269972450365b6",
  "0xc2c543d39426bfd1db66bbde2dd9e4a5c7212876",
  "0x3cc896f6761253d105737867e784cf2ffd8ca11e",
  "0x0171b64518477b66e4f7069a66585eac513d1d9a",
  "0xde35a0595a53676babf4f4cb0d4efa0b8db46e77",
  "0x2513271b9c0b5131f3e1a949179d53285cae2b23",
  "0xc6db514244f75c55688ecf2a379443551353ac4c",
  "0xc6db514244f75c55688ecf2a379443551353ac4c"
  // other Base NFT addresses
]

Fetch all Base NFT owners

Using the array of token addresses from the first step, you can fetch all Base NFT holders that hold any of the NFTs that the given user, e.g. vitalik.eth, owned on Base:

Try Demo

Code

query MyQuery($tokenAddresses: [Address!]) {
  TokenBalances(
    input: {
      filter: {
        tokenAddress: { _in: $tokenAddresses }
        tokenType: { _in: [ERC721] }
      }
      blockchain: base
      limit: 200
    }
  ) {
    TokenBalance {
      token {
        name
        address
        tokenNfts {
          tokenId
        }
        blockchain
        logo {
          small
        }
      }
      owner {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
}

The response then can be formatted further with the following formatting function to extract all the recommended users that has common Base NFTs with a given user:

utils/formatBaseNftData.js
const formatBaseNftData = (data, _recommendedUsers = []) => {
  const recommendedUsers = [..._recommendedUsers];

  for (const nft of data) {
    const { owner, token } = nft ?? {};
    const { name, logo, address, tokenNfts = [] } = token ?? {};
    const { addresses } = owner ?? {};
    const tokenNft = tokenNfts?.[0];

    const existingUserIndex = recommendedUsers.findIndex(
      ({ addresses: recommendedUsersAddresses }) =>
        recommendedUsersAddresses?.some?.((address) =>
          addresses?.includes?.(address)
        )
    );

    if (existingUserIndex !== -1) {
      const _addresses = recommendedUsers?.[existingUserIndex]?.addresses || [];
      recommendedUsers[existingUserIndex].addresses = [
        ..._addresses,
        ...addresses,
      ]?.filter((address, index, array) => array.indexOf(address) === index);
      const _nfts = recommendedUsers?.[existingUserIndex]?.nfts || [];
      const nftExists = _nfts.some((nft) => nft.address === address);
      if (!nftExists) {
        _nfts?.push({
          name,
          image: logo?.small,
          blockchain: "base",
          address,
          tokenNfts: tokenNft,
        });
      }
      recommendedUsers[existingUserIndex].nfts = [..._nfts];
    } else {
      recommendedUsers.push({
        ...owner,
        nfts: [
          {
            name,
            image: logo?.small,
            blockchain: "base",
            address,
            tokenNfts: tokenNft,
          },
        ],
      });
    }
  }
  return recommendedUsers;
};

export default formatBaseNftData;

The formatted result will have a format as follows:

[
  {
    "addresses": ["0xc0acf511babc340fd0ff969e112c4c45e31c6c7c"],
    "domains": null,
    "socials": null,
    "xmtp": null,
    // show all common Base NFTs that is also owned by vitalik.eth
    "nfts": [
      {
        "name": ".basepunk"",
        "image": null,
        "blockchain": "base",
        "address": "0xc2c543d39426bfd1db66bbde2dd9e4a5c7212876",
        "tokenNfts": {
          "tokenId": "220"
        }
      }
    ]
  },
  // other onchain graph users
]

Iterate to fetch all common Base NFT holders

With the queries for fetching all the common Base NFT holders that holds the same Base NFTs as the given user established, it will be essential to fetch all the data using paginations.

In order to paginate through all the data, you can utilize fetchQueryWithPagination and execute_paginated_query from the JavaScript (React & Node) and Python SDKs, respectively. The full code implementation for this will be as follows:

functions/formatBaseNftData.js
import { init, fetchQueryWithPagination } from "@airstack/node"; // or @airstack/airstack-react for frontend javascript
import formatBaseNftData from "./utils/formatBaseNftData";

// get your API key at https://app.airstack.xyz/profile-settings/api-keys
init("YOUR_AIRSTACK_API_KEY");

const nftAddressesQuery = `
query MyQuery($user: Identity!) {
  TokenBalances(input: {filter: {tokenType: {_in: [ERC721]}, owner: {_eq: $user}}, blockchain: base, limit: 200}) {
    TokenBalance {
      tokenAddress
    }
  }
}
`;

const nftQuery = `
query MyQuery($tokenAddresses: [Address!]) {
  TokenBalances(
    input: {filter: {tokenAddress: {_in: $tokenAddresses}, tokenType: {_in: [ERC721]}}, blockchain: base, limit: 200}
  ) {
    TokenBalance {
      token {
        name
        address
        tokenNfts {
          tokenId
        }
        blockchain
        logo {
          small
        }
      }
      owner {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
}
`;

const fetchBaseNft = async (address, existingUsers = []) => {
  let baseNftDataResponse;
  let recommendedUsers = [...existingUsers];
  while (true) {
    if (!baseNftDataResponse) {
      // Pagination #1: Fetch Base NFTs
      baseNftDataResponse = await fetchQueryWithPagination(
        nftAddressesQuery,
        {
          user: address,
        }
      );
    }
    const {
      data: baseNftData,
      error: baseNftError,
      hasNextPage: baseNftHasNextPage,
      getNextPage: baseNftGetNextPage,
    } = baseNftDataResponse ?? {};
    if (!baseNftError) {
      const tokenAddresses =
        baseNftData?.TokenBalances?.TokenBalance?.map(
          (token) => token.tokenAddress
        ) ?? [];
      let baseNftHoldersDataResponse;
      while (true) {
        if (tokenAddresses.length === 0) break;
        if (!baseNftHoldersDataResponse) {
          // Pagination #2: Fetch Base NFT Holders
          baseNftHoldersDataResponse = await fetchQueryWithPagination(
            nftQuery,
            {
              tokenAddresses,
            }
          );
        }
        const {
          data: baseNftHoldersData,
          error: baseNftHoldersError,
          hasNextPage: baseNftHoldersHasNextPage,
          getNextPage: baseNftHoldersGetNextPage,
        } = baseNftHoldersDataResponse;
        if (!baseNftHoldersError) {
          recommendedUsers = [
            ...formatBaseNftData(
              baseNftHoldersData?.TokenBalances?.TokenBalance,
              recommendedUsers
            ),
          ];
          if (!baseNftHoldersHasNextPage) {
            break;
          } else {
            baseNftHoldersDataResponse =
              await baseNftHoldersGetNextPage();
          }
        } else {
          console.error("Error: ", baseNftHoldersError);
          break;
        }
      }
      if (!baseNftHasNextPage) {
        break;
      } else {
        baseNftDataResponse = await baseNftGetNextPage();
      }
    } else {
      console.error("Error: ", baseNftError);
      break;
    }
  }
  return recommendedUsers;
};

export default fetchBaseNft;

Step 1.10: Fetch Common Zora Token Holders Data

You can use Airstack to fetch all the NFTs that are hold by a given user, e.g. vitalik.eth, on Zora:

Try Demo

Code

query MyQuery($user: Identity!) {
  TokenBalances(
    input: {
      filter: { tokenType: { _in: [ERC721] }, owner: { _eq: $user } }
      blockchain: zora
      limit: 200
    }
  ) {
    TokenBalance {
      tokenAddress
    }
  }
}

Then, the response can be filtered to only Zora NFTs and be formatted into an array of token addresses to be used in the next step:

const tokenAddresses =
  data?.TokenBalances?.TokenBalance?.map((token) => token.tokenAddress) ?? [];

where data is the response from the API. The formatted result will have a format as follows:

[
  "0x87c7d8006e3d96811110f419d667c86f7a07d325",
  "0x491b247de8995c3a438bc12e3375217a000cdbd0",
  "0x9565b41bdb9e79e8b877ddbdaf2af81f44368f94",
  "0x47764e368cfbc98e601462c107452f4f0ddd1632",
  "0xbfb2b3c41e8d61a23750f3723be1f40b0f86ab5f",
  "0xca803ff1db7943d997803b4d53940c1b57151538",
  "0xd387dca83813f541035299bf0e0d073d3cd3b8e0",
  "0xf421f9041bef1380756b31d11d5a3f06cc11a241",
  "0xb6040323ce2e79357faeec1490b1ecf3936969cb",
  "0xad0a41328a40f6d18f3458529470173e88fff53f",
  "0x00a958d6199a700588c303904d41405d80f47278"
  // other Zora NFT addresses
]

Fetch all Zora NFT owners

Using the array of token addresses from the first step, you can fetch all Zora NFT holders that hold any of the NFTs that the given user, e.g. vitalik.eth, owned on Zora:

Try Demo

Code

query MyQuery($tokenAddresses: [Address!]) {
  TokenBalances(
    input: {
      filter: {
        tokenAddress: { _in: $tokenAddresses }
        tokenType: { _in: [ERC721] }
      }
      blockchain: zora
      limit: 200
    }
  ) {
    TokenBalance {
      token {
        name
        address
        tokenNfts {
          tokenId
        }
        blockchain
        logo {
          small
        }
      }
      owner {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
}

The response then can be formatted further with the following formatting function to extract all the recommended users that has common Zora NFTs with a given user:

utils/formatZoraNftData.js
const formatZoraNftData = (data, _recommendedUsers = []) => {
  const recommendedUsers = [..._recommendedUsers];

  for (const nft of data) {
    const { owner, token } = nft ?? {};
    const { name, logo, address, tokenNfts = [] } = token ?? {};
    const { addresses } = owner ?? {};
    const tokenNft = tokenNfts?.[0];

    const existingUserIndex = recommendedUsers.findIndex(
      ({ addresses: recommendedUsersAddresses }) =>
        recommendedUsersAddresses?.some?.((address) =>
          addresses?.includes?.(address)
        )
    );

    if (existingUserIndex !== -1) {
      const _addresses = recommendedUsers?.[existingUserIndex]?.addresses || [];
      recommendedUsers[existingUserIndex].addresses = [
        ..._addresses,
        ...addresses,
      ]?.filter((address, index, array) => array.indexOf(address) === index);
      const _nfts = recommendedUsers?.[existingUserIndex]?.nfts || [];
      const nftExists = _nfts.some((nft) => nft.address === address);
      if (!nftExists) {
        _nfts?.push({
          name,
          image: logo?.small,
          blockchain: "zora",
          address,
          tokenNfts: tokenNft,
        });
      }
      recommendedUsers[existingUserIndex].nfts = [..._nfts];
    } else {
      recommendedUsers.push({
        ...owner,
        nfts: [
          {
            name,
            image: logo?.small,
            blockchain: "zora",
            address,
            tokenNfts: tokenNft,
          },
        ],
      });
    }
  }
  return recommendedUsers;
};

export default formatZoraNftData;

The formatted result will have a format as follows:

[
  {
    "addresses": ["0xf127f1e31aef9f2bd25b10e09baa606e38de62c4"],
    "domains": null,
    "socials": null,
    "xmtp": null,
    // show all common Zora NFTs that is also owned by vitalik.eth
    "nfts": [
      {
        "name": "Zeon Face"",
        "image": null,
        "blockchain": "zora",
        "address": "0x491b247de8995c3a438bc12e3375217a000cdbd0",
        "tokenNfts": {
          "tokenId": "45"
        }
      }
    ]
  },
  // other onchain graph users
]

Iterate to fetch all common Zora NFT holders

With the queries for fetching all the common Zora NFT holders that holds the same Zora NFTs as the given user established, it will be essential to fetch all the data using paginations.

In order to paginate through all the data, you can utilize fetchQueryWithPagination and execute_paginated_query from the JavaScript (React & Node) and Python SDKs, respectively. The full code implementation for this will be as follows:

functions/fetchZoraNftData.js
import { init, fetchQueryWithPagination } from "@airstack/node"; // or @airstack/airstack-react for frontend javascript
import formatZoraNftData from "./utils/formatZoraNftData";

// get your API key at https://app.airstack.xyz/profile-settings/api-keys
init("YOUR_AIRSTACK_API_KEY");

const nftAddressesQuery = `
query MyQuery($user: Identity!) {
  TokenBalances(input: {filter: {tokenType: {_in: [ERC721]}, owner: {_eq: $user}}, blockchain: zora, limit: 200}) {
    TokenBalance {
      tokenAddress
    }
  }
}
`;

const nftQuery = `
query MyQuery($tokenAddresses: [Address!]) {
  TokenBalances(
    input: {filter: {tokenAddress: {_in: $tokenAddresses}, tokenType: {_in: [ERC721]}}, blockchain: zora, limit: 200}
  ) {
    TokenBalance {
      token {
        name
        address
        tokenNfts {
          tokenId
        }
        blockchain
        logo {
          small
        }
      }
      owner {
        addresses
        domains {
          name
          isPrimary
        }
        socials {
          dappName
          blockchain
          profileName
          profileImage
          profileTokenId
          profileTokenAddress
        }
        xmtp {
          isXMTPEnabled
        }
      }
    }
  }
}
`;

const fetchZoraNft = async (address, existingUsers = []) => {
  let zoraNftDataResponse;
  let recommendedUsers = [...existingUsers];
  while (true) {
    if (!zoraNftDataResponse) {
      // Pagination #1: Fetch Zora NFTs
      zoraNftDataResponse = await fetchQueryWithPagination(
        nftAddressesQuery,
        {
          user: address,
        }
      );
    }
    const {
      data: zoraNftData,
      error: zoraNftError,
      hasNextPage: zoraNftHasNextPage,
      getNextPage: zoraNftGetNextPage,
    } = zoraNftDataResponse ?? {};
    if (!zoraNftError) {
      const tokenAddresses =
        zoraNftData?.TokenBalances?.TokenBalance?.map(
          (token) => token.tokenAddress
        ) ?? [];
      let zoraNftHoldersDataResponse;
      while (true) {
        if (tokenAddresses.length === 0) break;
        if (!zoraNftHoldersDataResponse) {
          // Pagination #2: Zora Base NFT Holders
          zoraNftHoldersDataResponse = await fetchQueryWithPagination(
            nftQuery,
            {
              tokenAddresses,
            }
          );
        }
        const {
          data: zoraNftHoldersData,
          error: zoraNftHoldersError,
          hasNextPage: zoraNftHoldersHasNextPage,
          getNextPage: zoraNftHoldersGetNextPage,
        } = zoraNftHoldersDataResponse;
        if (!zoraNftHoldersError) {
          recommendedUsers = [
            ...formatBaseNftData(
              zoraNftHoldersData?.TokenBalances?.TokenBalance,
              recommendedUsers
            ),
          ];
          if (!zoraNftHoldersHasNextPage) {
            break;
          } else {
            zoraNftHoldersDataResponse =
              await zoraNftHoldersGetNextPage();
          }
        } else {
          console.error("Error: ", zoraNftHoldersError);
          break;
        }
      }
      if (!zoraNftHasNextPage) {
        break;
      } else {
        zoraNftDataResponse = await zoraNftGetNextPage();
      }
    } else {
      console.error("Error: ", zoraNftError);
      break;
    }
  }
  return recommendedUsers;
};

export default fetchZoraNft;

Step 2: Aggregate All Data By User Identities

In the previous step, you have successfully create multiple functions to fetch a user's onchain and off-chain data, from POAPs to Lens and Farcasters followers.

In this step, you'll use the data from Step 1 to aggregate all the data fetched and compile it into the given user's onchain graph.

Utilizing the data fetching functions that we have defined, we can easily import them into a single file and do an iterative call on every function step-by-step as shown below:

index.js
import fetchPoapsData from "./functions/fetchPoapsData";
import fetchFarcasterFollowings from "./functions/fetchFarcasterFollowings";
import fetchLensFollowings from "./functions/fetchLensFollowings";
import fetchFarcasterFollowers from "./functions/fetchFarcasterFollowers";
import fetchLensFollowers from "./functions/fetchLensFollowers";
import fetchTokenSent from "./functions/fetchTokenSent";
import fetchTokenReceived from "./functions/fetchTokenReceived";
import fetchEthNft from "./functions/fetchEthNft";
import fetchBaseNft from "./functions/fetchBaseNft";
import fetchZoraNft from "./functions/fetchZoraNft";

const fetchOnChainGraphData = async (address) => {
  let recommendedUsers = [];
  const fetchFunctions = [
    fetchPoapsData,
    fetchFarcasterFollowings,
    fetchLensFollowings,
    fetchFarcasterFollowers,
    fetchLensFollowers,
    fetchTokenSent,
    fetchTokenReceived,
    fetchEthNft,
    fetchBaseNft,
    fetchZoraNft,
  ];
  for (const func of fetchFunctions) {
    recommendedUsers = await func(address, recommendedUsers);
  }
  return recommendedUsers;
};

const onChainGraphUsers = await fetchOnChainGraphData("vitalik.eth");

Through the for loops, the recommendedUsers(JavaScript) and recommended_users(Python) variable will be storing onchain graph users and have their data updated whenever new data is fetched.

Step 3: Scoring & Sorting

Now that you have all the data aggregated, you might notice that some of those users from the onchain graph might have higher relevancies to the given user, such as having more POAPs in common than the given user.

Thus, for a better user experience, it will make more sense to score individual user profiles and with the scoring system established, sort them in descending order (from the highest score/most relevant to the lowest score/least relevant).

Scoring

In this tutorial, let's establish a scoring function that will calculate the total score of individual users on the onchain graph as follows:

score(x)=โˆ‘(pointsโˆ—weight)score(x) = \sum (points * weight)

Each data has different methods to calculate points and has their individual weights:

DataPointsWeight (Default)

Token sent

1

10

Token received

1

0

Followed on Lens

1

5

Following on Lens

1

7

Followed on Farcaster

1

5

Following on Farcaster

1

5

Common POAPs

number of POAPs hold

7

Common Ethereum NFTs

number of Ethereum NFTs hold

5

Common Base NFTs

number of Base NFTs hold

3

Common Zora NFTs

number of Zora NFTs hold

3

Thus, translating this into code, the score calculation function will look as follows:

score.js
const defaultScoreMap = {
  tokenSent: 10,
  tokenReceived: 0,
  followedByOnLens: 5,
  followingOnLens: 7,
  followedByOnFarcaster: 5,
  followingOnFarcaster: 5,
  commonPoaps: 7,
  commonEthNfts: 5,
  commonBaseNfts: 3,
  commonZoraNfts: 3,
};

const identityMap = (identities) =>
  identities.reduce((acc, identity) => {
    acc[identity] = true;
    return acc;
  }, {});

const isBurnedAddress = (address) => {
  if (!address) {
    return false;
  }
  address = address.toLowerCase();
  return (
    address === "0x0000000000000000000000000000000000000000" ||
    address === "0x000000000000000000000000000000000000dead"
  );
};

const calculatingScore = (user, scoreMap = defaultScoreMap) => {
  const identities = [user];
  if (
    user.addresses?.some((address) => identityMap(identities)[address]) ||
    user.domains?.some(({ name }) => identityMap(identities)[name]) ||
    user.addresses?.some((address) => isBurnedAddress(address))
  ) {
    return;
  }

  let score = 0;
  if (user.follows?.followingOnLens) {
    score += scoreMap.followingOnLens;
  }
  if (user.follows?.followedOnLens) {
    score += scoreMap.followedByOnLens;
  }
  if (user.follows?.followingOnFarcaster) {
    score += scoreMap.followingOnFarcaster;
  }
  if (user.follows?.followedOnFarcaster) {
    score += scoreMap.followedByOnFarcaster;
  }
  if (user.tokenTransfers?.sent) {
    score += scoreMap.tokenSent;
  }
  if (user.tokenTransfers?.received) {
    score += scoreMap.tokenReceived;
  }
  let uniqueNfts = [];
  if (user.nfts) {
    const existingNFT = {};
    uniqueNfts = user.nfts.filter((nft) => {
      const key = `${nft.address}-${nft.tokenNfts?.tokenId}`;
      if (existingNFT[key] || isBurnedAddress(nft.address)) {
        return false;
      }
      existingNFT[key] = true;
      return true;
    });

    const ethNftCount = uniqueNfts.filter(
      (nft) => nft.blockchain === "ethereum"
    ).length;
    const baseNftCount = uniqueNfts.filter(
      (nft) => nft.blockchain === "base"
    ).length;
    const zoraNftCount = uniqueNfts.filter(
      (nft) => nft.blockchain === "zora"
    ).length;
    score +=
      scoreMap.commonEthNfts * ethNftCount +
      scoreMap.commonBaseNfts * baseNftCount +
      scoreMap.commonZoraNfts * zoraNftCount;
  }
  if (user.poaps) {
    score += scoreMap.commonPoaps * user.poaps.length;
  }

  return {
    ...user,
    _score: score,
  };
};

export default calculatingScore;

To assign score to each user, you can simply use the following code:

index.js
import calculatingScore from "score";

const onChainGraphUsers = await fetchOnChainGraphData("vitalik.eth");
const onChainGraphUsersWithScore = recommendUsers.map(user => calculatingScore(user));
console.log(onChainGraphUsersWithScore);

and the modified JSON will have a new _score field as follows:

[
  {
    "addresses": ["0xd35f7c2f23fdc341aa8c7534f0e521679206a036"],
    "domains": [{ "name": "taoliu.eth", "isPrimary": true }],
    "socials": [
      {
        "dappName": "lens",
        "blockchain": "polygon",
        "profileName": "lens/@colinlt",
        "profileImage": "",
        "profileTokenId": "33481",
        "profileTokenAddress": "0xdb46d1dc155634fbc732f92e853b10b288ad5a1d"
      }
    ],
    "xmtp": null,
    "poaps": [
      {
        "name": "Rocket Pool Bot Catcher POAP",
        "image": null,
        "eventId": "7426"
      }
    ],
    "_score": 7 // onchain graph score of the user `taoliu.eth`
  },
  {
    "addresses": ["0x263af7a0ba6f8432e7861b9d92a44639c768d17f"],
    "domains": null,
    "socials": null,
    "xmtp": null,
    "poaps": [
      {
        "name": "Messi Win FWC QATAR 2022",
        "image": "https://assets.airstack.xyz/image/poap/qSZIZX20XcTPh6qr55uhXw==/extra_small.png",
        "eventId": "92705"
      }
    ],
    "_score": 7 // onchain graph score of the user `0x263af7a0ba6f8432e7861b9d92a44639c768d17f`
  },
  {
    "addresses": ["0xd5aec8ceb2a5dee7914d1c5d07db7e3391253f31"],
    "domains": null,
    "socials": null,
    "xmtp": null,
    "poaps": [
      {
        "name": "Rocket Pool Bot Catcher POAP",
        "image": null,
        "eventId": "7426"
      }
    ],
    "_score": 7 // onchain graph score of the user `0xd5aec8ceb2a5dee7914d1c5d07db7e3391253f31`
  }
]

Sorting

Lastly, once you have all the recommended users' score calculated, you can use the following sorting function that will return the sorted result of the onchain graph:

sort.js
const sortByScore = (recommendations) => {
  return recommendations.sort((a, b) => {
    return (b._score || 0) - (a._score || 0);
  });
};

export default sortByScore;

Import it to the index file as follows:

index.js
import sortByScore from "sort";

const onChainGraphUsers = await fetchOnChainGraphData("vitalik.eth");
const onChainGraphUsersWithScore = recommendUsers.map(user => calculatingScore(user));
// finalOnChainGraphUsers can be stored in database
const finalOnChainGraphUsers = sortByScore(onChainGraphUsersWithScore);
console.log(finalOnChainGraphUsers);

If you are doing backend integration, you can store the finalOnChainGraphUsers(JavaScript) or final_on_chain_graph_users(Python) that contains the fully-processed onchain graph data into your database.

The sorted final result will look as shown below:

[
  {
    "addresses": ["0xf6b6f07862a02c85628b3a9688beae07fea9c863"],
    "domains": [
      { "name": "juliuspreite.eth", "isPrimary": false },
      { "name": "poap.mirror.xyz", "isPrimary": false },
      { "name": "poap.sismo.eth", "isPrimary": false },
      // other ENS domains
    ],
    "socials": [
      {
        "dappName": "farcaster",
        "blockchain": "optimism",
        "profileName": "worthalter",
        "profileImage": "https://i.imgur.com/5ywjJJD.jpg",
        "profileTokenId": "9456",
        "profileTokenAddress": "0x00000000fcaf86937e41ba038b4fa40baa4b780a"
      }
    ],
    "xmtp": [{ "isXMTPEnabled": true }],
    "poaps": [
      {
        "name": "ETHWaterloo 2019",
        "image": "https://assets.airstack.xyz/image/poap/jJBtpGTUG7nxFlXa6q+pvQ==/extra_small.png",
        "eventId": "84"
      },
      {
        "name": "TEL AVIV BLOCKCHAIN WEEK 2019",
        "image": "https://assets.airstack.xyz/image/poap/DCDoHTJADUNCSJKdWXTang==/extra_small.png",
        "eventId": "65"
      },
      {
        "name": "Zcon0",
        "image": "https://assets.airstack.xyz/image/poap/UEKJYWIB9mwSux2YThOnZw==/extra_small.png",
        "eventId": "36"
      },
      // other POAPs
    ],
    "_score": 56 // The highest score comes first
  },
  {
    "addresses": ["0x225f137127d9067788314bc7fcc1f36746a3c3b5"],
    "domains": [
      { "name": "stopspammingyourname.eth", "isPrimary": false },
      { "name": "conferencewifi.eth", "isPrimary": false },
      { "name": "bestfuckever.eth", "isPrimary": false },
      // other ENS domains
    ],
    "socials": [
      {
        "dappName": "farcaster",
        "blockchain": "optimism",
        "profileName": "lucemans",
        "profileImage": "https://i.imgur.com/GOu2pEH.jpg",
        "profileTokenId": "20737",
        "profileTokenAddress": "0x00000000fcaf86937e41ba038b4fa40baa4b780a"
      },
      {
        "dappName": "lens",
        "blockchain": "polygon",
        "profileName": "lens/@lucemans",
        "profileImage": "",
        "profileTokenId": "12083",
        "profileTokenAddress": "0xdb46d1dc155634fbc732f92e853b10b288ad5a1d"
      }
    ],
    "xmtp": [{ "isXMTPEnabled": true }],
    "poaps": [
      {
        "name": "I met tjais.eth at Devcon 6",
        "image": "https://assets.airstack.xyz/image/poap/1O7dHBavxRVTxH12cq+RoA==/extra_small.png",
        "eventId": "71115"
      },
      {
        "name": "I met nicogallardo.eth at Devcon 6",
        "image": "https://assets.airstack.xyz/image/poap/o5X7XVS3H3umU7iRKeoaPw==/extra_small.png",
        "eventId": "69822"
      },
      {
        "name": "I met helenag.eth at Devcon 6",
        "image": "https://assets.airstack.xyz/image/poap/pX4g6cy3zHCyDIqioCbY4g==/extra_small.png",
        "eventId": "74216"
      },
      // other POAPs
    ],
    "_score": 49 // lowers score comes after
  },
  // more onchain graph recommended users
]

And done! ๐ŸŽ‰ ๐Ÿฅณ Congratulations you've just built an onchain graph!

Developer Support

If you have any questions or need help regarding integrating or building onchain graph into your application, please join our Airstack's Telegram group.

More Resources

Last updated

Was this helpful?