πŸ•ΈοΈOnchain Graph

Learn how to recommend mints that are trending amongst the user's onchain connections (social follows, POAPs in common, NFTs in common, token transfers, and more).

Table Of Contents

In this tutorial, you'll learn how to build trending mints based on your user's onchain graph for your web3 application using either JavaScript/TypeScript or Python:

Currently, there is no dedicated backend API for fetching trending mints directly into your application. Therefore, the following implementation will require you to run a backend.

For the backend, you will be required to run a cronjob to fetch token mints data from the Airstack API and have the data stored in a dedicated database. This turotial will walk you through the steps required to implement Trending Mints today and deliver immediate value to your users.\

Concurrently Airstack is working on a dedicated Trending Mints API for lighter-weight integrations in the near future.

Pre-requisites

Get Started

To get started, install the Airstack SDK:

npm install @airstack/node dayjs node-cron

Step 1: Fetch All Recent Token Mints Minted By Onchain Graph Users

First, define the following parameters to fetch the token mints data:

Intervals

The interval that you would like to run your cron job. Using the interval, you can then set the variables for the query that will be shown below:

  • endTime to the current unix timestamp

  • startTime to the current unix timestamp minus the chosen interval duration.

In this tutorial, we'll use 1 hour as the default interval.

Token Types

Input all the token types that you would like to fetch from the mints data.

If you only prefer fungible token mints, then includes only ERC20. If you instead want to enable NFT mints only, then include both ERC721 and ERC1155.

Chains

Choose the chain that you would like to fetch the token mints data.

Currently, Airstack supports Ethereum, Base, Degen Chain, and other Airstack-supported chains.

Limit

The number of JSON object responses per API call, with a maximum allowable value of 200.


As these parameters are going to be having constant values, you can create a new file to store these as constant variables that can be imported in the next steps:

constant.ts
export const interval = 1; // 1 hour
export const tokenType = ["ERC20", "ERC721", "ERC1155"];
export const chains = ["ethereum", "gold", "base", "zora"];
export const limit = 200;

Fetching

As defined in the pre-requisites, you are expected to have a working implementation on onchain graph. If you have not, please follow this tutorial before continuing.

Assuming that you have the onchain graph setup and stored on your backend, you can fetch the full list of the onchain graph users' addresses to be provided as a variable to the query that will be shown next.

Along with the defined parameters, you can use TokenTransfers API to construct an Airstack query to fetch all recent tokens minted by all the onchain graph users of a given user in a certain interval period:

Try Demo

Show me minted tokens on Ethereum by an onchain graph user at certain timestamp

Code

query MyQuery(
  $startTime: Time,
  $endTime: Time,
  $tokenType: [TokenType!],
  $chain: TokenBlockchain!,
  $limit: Int,
  $onchainGraphUser: Identity
) {
  TokenTransfers(
    input: {
      filter: {
        # Only get token transfers that are mints
        from: {_eq: "0x0000000000000000000000000000000000000000"},
        blockTimestamp: {_gte: $startTime, _lte: $endTime},
        tokenType: {_in: $tokenType},
        to: {_eq: $onchainGraphUser},
        operator: {_eq: $onchainGraphUser},
      }
      blockchain: $chain,
      order: {blockTimestamp: DESC},
      limit: $limit
    }
  ) {
    TokenTransfer {
      tokenAddress
      token {
        name
      }
    }
  }
}

Iterate

Once you have the query ready, you can combine them in one main function to be executed in a single flow.

To fetch all the data, the query will be iterated multiple times using the fetchQueryWithPagination function provided by the Airstack SDK to iterate over all the blockchains and the paginations.

index.ts
import { init, fetchQueryWithPagination } from "@airstack/node";
import { config } from "dotenv";
import {
  interval,
  tokenType,
  chains,
  limit
} from "./constant";
import dayjs, { Dayjs } from "dayjs";

config();

init(process.env.AIRSTACK_API_KEY);

const query = `
query MyQuery(
  $startTime: Time,
  $endTime: Time,
  $tokenType: [TokenType!],
  $chain: TokenBlockchain!,
  $limit: Int,
  $onchainGraphUser: Identity
) {
  TokenTransfers(
    input: {
      filter: {
        from: {_eq: "0x0000000000000000000000000000000000000000"},
        blockTimestamp: {_gte: $startTime, _lte: $endTime},
        tokenType: {_in: $tokenType},
        to: {_eq: $onchainGraphUser},
        operator: {_eq: $onchainGraphUser},
      }
      blockchain: $chain,
      order: {blockTimestamp: DESC},
      limit: $limit
    }
  ) {
    TokenTransfer {
      tokenAddress
      token {
        name
      }
    }
  }
}
`;

const main = async (user: string, currentTime: Dayjs) => {
  let response;
  let mintsData = [];
  /**
   * You should fetch onchain graph users based on the `user` variable
   * with their onchain score from your DB here.
   * 
   * For this tutorial, this will simply user a constant variable.
   */
  const onchainGraphUsers = [
    {
      address: "0xb59aa5bb9270d44be3fa9b6d67520a2d28cf80ab",
      onchainScore: 96,
    },
    {
      address: "0xf6b6f07862a02c85628b3a9688beae07fea9c863",
      onchainScore: 64,
    },
    {
      address: "0xcbfbcbfca74955b8ab75dec41f7b9ef36f329879",
      onchainScore: 60,
    },
    // Other onchain graph users
  ];
  // Iterate over all onchain graph users
  for (let onchainGraphUser of onchainGraphUsers) {
    const { address, onchainScore } = onchainGraphUser ?? {};
    // Iterate over all blockchain
    for (let chain of chains) {
      while (true) {
        if (!response) {
          response = await fetchQueryWithPagination(query, {
            startTime: dayjs(currentTime?.subtract(interval, "h")).format(
              "YYYY-MM-DDTHH:mm:ss[Z]"
            ),
            endTime: currentTime?.format("YYYY-MM-DDTHH:mm:ss[Z]"),
            chain,
            limit,
            tokenType,
            onchainGraphUser,
          });
        }

        const { data, error, getNextPage, hasNextPage } = response ?? {};
        if (!error) {
          // aggregate all the token mints to `mintsData` variable
          mintsData = [
            ...mintsData,
            ...(data?.TokenTransfers?.TokenTransfer?.map((mint) => ({
              ...mint,
              minter: {
                address,
                onchainScore,
              },
              chain,
            })) ?? []),
          ];
          // Iterate over all paginations
          if (!hasNextPage) {
            break;
          } else {
            response = await getNextPage();
          }
        } else {
          console.error("Error: ", error);
          break;
        }
      }

      // Resetting the loop
      response = null;
    }
  }
  
  return mintsData;
}

export default main;

Step 2: Score, Sort, and Filter Token Mints

After fetching all the raw token mints data from Airstack API, next you can process the data to determine which will qualify as trending mints.

In this tutorial, there will be 3 steps to process the token mints data:

  1. Scoring – assigning a score to each minted tokens to determine how "popular" each tokens are in a certain period of time

  2. Sorting – sort the minted tokens data by the score, in descending order

  3. Filtering – filter out all non-trending tokens that does not qualify

These procedures are NOT a strict requirement and can be defined by yourself depending on the requirements you have for building the feature into application.

Scoring

In this tutorial, we'll define the scoring function for a minted token to be the sum of the multiplication of the minter's onchain graph score and a token's mint frequency by the minter in the defined interval:

You are not required to follow the scoring logic shown in this tutorial. Depending on your use cases, you are free to create your own custom scoring function or skip the scoring step all together if unnecessary.

utils/scoring.ts
export interface Data {
  TokenTransfers: TokenTransfer;
}

export interface TokenTransfer {
  TokenTransfer: TokenTransferDetails[];
}

export interface TokenTransferDetails {
  tokenAddress: string;
  operator: Identity;
  to: Identity;
  token: Token;
}

export interface Identity {
  addresses: string;
}

export interface Token {
  name: string;
}

/**
 * @description
 * Score all recent token mint data by how much people mint the token.
 * Each minting represent a score of 1.
 *
 * @example
 * const res = scoringFunction(data);
 *
 * @param {Object} data – Formatted minted tokens data from Airstack API
 * @returns scored mint data
 */
const scoringFunction = (data: TokenTransfer[]) => {
  let trendingMints = [];

  data.forEach((val) => {
    const { tokenAddress, chain, minter } = val ?? {};
    const { address, onchainScore } = minter ?? {};
    const valIndex = trendingMints.findIndex((value) => {
      return value?.tokenAddress === tokenAddress && value?.chain === chain;
    });
    if (valIndex !== -1) {
      const minterIndex = trendingMints?.[valIndex]?.minters?.findIndex(
        (minter) => {
          return (
            minter?.address === address && minter?.onchainScore === onchainScore
          );
        }
      );
      trendingMints[valIndex] = {
        ...trendingMints[valIndex],
        // For each new mints, add score of the minter's onchain score
        score: trendingMints[valIndex]?.score + onchainScore,
        minters: [
          ...trendingMints[valIndex]?.minters,
          // only add the minter to the list it is a new minter
          minterIndex !== -1 ? null : minter,
        ]?.filter(Boolean),
      };
    } else {
      delete val?.minter;
      // For new token mints, assigned initial score of the minter's onchain score
      trendingMints.push({ ...val, score: onchainScore, minters: [minter] });
    }
  });

  return trendingMints;
};

export default scoringFunction;

Then, you can import the scoringFunction back to main to have the data from Airstack API scored:

index.ts
// same imports as above
import scoringFunction from "./utils/scoring";

const main = (user: string, currentTime: Dayjs) = > {
  // same as above
  const scoredData = scoringFunction(mintsData);
  return scoredData
}

export default main;

When the result of the newly modified main function is logged, it will have result that look as follows:

[
  {
    "tokenAddress": "0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401",
    "token": { "name": "NameWrapper" },
    "chain": "ethereum",
    "score": 52,
    "minters": [
      {
        "address": "0x171dd9a138b796e3b307086b136a810bae44a185",
        "onchainScore": 52
      }
    ]
  },
  {
    "tokenAddress": "0x9a74559843f7721f69651eca916b780ef78bd060",
    "token": { "name": "Poglin: Battle For Havens Destiny" },
    "chain": "ethereum",
    "score": 52,
    "minters": [
      {
        "address": "0x171dd9a138b796e3b307086b136a810bae44a185",
        "onchainScore": 52
      }
    ]
  }
  // Other scored token mints data
]

Sorting

Once you have the token mints data scored, you can implement a very simple sorting function that sorts in descending order based on the score field value:

utils/sorting.ts
import { TokenTransferDetails } from "./scoring";

export interface TokenTransferWithScore extends TokenTransferDetails {
  score: number;
}

/**
 * @description
 * Sort all scored mints data by `score` field
 *
 * @example
 * const res = sortingFunction(scoredData);
 *
 * @param {Object} data – Minted tokens data with `score` field
 * @returns scored and sorted mint data
 */
const sortingFunction = (scoredData: TokenTransferWithScore) =>
  scoredData?.sort((a, b) => b.score - a.score);

export default sortingFunction;

Then, you can import the sortingFunction back to main to have the scored data sorted:

index.ts
// same imports as above
import { sortingFunction } from "./utils/sorting";

const main = (user: string, currentTime: Dayjs) = > {
  // same as above
  const sortedData = sortingFunction(scoredData);
  return sortedData
}

export default main;

When the result of the newly modified main function is logged, it will have result that look as follows:

[
  {
    "tokenAddress": "0xc347075b60ff7f07eea970636ea9a8f95d7e7da9",
    "token": { "name": "Shadowink" },