👛Common Minters

Learn how to recommend mints based on Lookalike audiences: users who have X are also Minting Y.

Table Of Contents

The algorithm for building trending mints will be as follows:

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 Common Minters

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, you'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

First, you will need to fetch all the common minters that minted the same tokens as the given user.

To fetch all common minters, first fetch all the tokens minted by the user by using the TokenTransfers API and provide the user's address to the $user variable:

Try Demo

Show me all tokens minted by user on Ethereum

Code

query MyQuery(
  $user: Identity!
  $tokenType: [TokenType!]
  $chain: TokenBlockchain!
  $limit: Int
) {
  TokenTransfers(
    input: {
      filter: {
        operator: { _eq: $user }
        from: { _eq: "0x0000000000000000000000000000000000000000" }
        to: { _eq: $user }
        tokenType: { _in: $tokenType }
      }
      blockchain: $chain
      order: { blockTimestamp: DESC }
      limit: $limit
    }
  ) {
    TokenTransfer {
      tokenAddress
      token {
        name
      }
    }
  }
}

With this result, you can then format it to form an array of addresses using formatUserMints function:

utils/format.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 Format user mints to an array of token addresses
 * @examples
 * const { data } = await fetchQuery(query, variables);
 * formatUserMints(data);
 *
 * @param {Object} data - All minted tokens by a user from Airstack API
 * @returns An array of minted token addresses
 */
export const formatUserMints = (data: Data) =>
  data?.TokenTransfers?.TokenTransfer?.map(({ tokenAddress }) => tokenAddress);

And the output of formatUserMints function will look as follows:

[
  "0x0f92612c5f539c89dc379e8aa42e3ba862a34b7e",
  "0xc9b09c916e22eb7b68037275fe035eb30d3989f7",
  "0xebb15487787cbf8ae2ffe1a6cca5a50e63003786",
  "0xad08067c7d3d3dbc14a9df8d671ff2565fc5a1ae",
  "0x9340204616750cb61e56437befc95172c6ff6606"
  // other minted token addresses
]

With the obtained array of minted token addresses, then you can use TokenTransfers again to fetch all the addresses that also minted the minted tokens as the given users. In other words, the common minters to the given user:

Try Demo

Show all minters that minted an array of given tokens on Ethereum

Code

query MyQuery($mintedToken: Address!, $chain: TokenBlockchain!, $limit: Int) {
  TokenTransfers(
    input: {
      filter: {
        from: { _eq: "0x0000000000000000000000000000000000000000" }
        tokenAddress: { _eq: $mintedToken }
      }
      blockchain: $chain
      order: { blockTimestamp: DESC }
      limit: $limit
    }
  ) {
    TokenTransfer {
      tokenAddress
      token {
        name
      }
      operator {
        addresses
      }
      to {
        addresses
      }
    }
  }
}

From here, you can use the data to get all the minters by checking if the operator and receiver address is equal and further categorize the list of minters by individual minted tokens.


With the defined parameters, you can use the TokenTransfers API again to construct an Airstack query to fetch all recent tokens minted by all the common minters of a given user in a certain interval period by providing the individual minter 0x addresses from formatCommonMinters to the $commonMinters variable:

Try Demo

Show me minted tokens on Ethereum by a common minter at certain timestamp

Code

query MyQuery(
  $startTime: Time,
  $endTime: Time,
  $tokenType: [TokenType!],
  $chain: TokenBlockchain!,
  $limit: Int,
  $commonMinter: Identity
) {
  TokenTransfers(
    input: {
      filter: {
        # Only get token transfers that are mints
        from: {_eq: "0x0000000000000000000000000000000000000000"},
        blockTimestamp: {_gte: $startTime, _lte: $endTime},
        tokenType: {_in: $tokenType},
        to: {_eq: $commonMinter},
        operator: {_eq: $commonMinter},
      }
      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.

Before defining the main function, create a separate function to fetch the list of all common minters. 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.

functions/fetchCommonMinters.ts
import { init, fetchQueryWithPagination } from "@airstack/node";
import { config } from "dotenv";
import { chains, tokenType, limit } from "../constant";

config();

init(process.env.AIRSTACK_API_KEY);

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

const commonMintersQuery = `
query MyQuery(
  $mintedToken: Address!,
  $chain: TokenBlockchain!,
  $limit: Int!
) {
  TokenTransfers(
    input: {
      filter: {
        from: {_eq: "0x0000000000000000000000000000000000000000"},
        tokenAddress: {_eq: $mintedToken},
      },
      blockchain: $chain,
      order: {blockTimestamp: DESC},
      limit: $limit
    }
  ) {
    TokenTransfer {
      tokenAddress
      token {
        name
      }
      operator {
        addresses
      }
      to {
        addresses
      }
    }
  }
}
`;

/**
 * @description Fetches common minters associated with individual minted token by `user`
 * @example
 * const res = await fetchCommonMinters("0xB59Aa5Bb9270d44be3fA9b6D67520a2d28CF80AB");
 *
 * @param {string} user – User's 0x address
 * @returns Common minters associated with individual minted token by `user`
 */
const fetchCommonMinters = async (user: string) => {
  let mintedTokensDataResponse;
  let mintsData = [];
  for (let chain of chains) {
    while (true) {
      if (!mintedTokensDataResponse) {
        mintedTokensDataResponse = await fetchQueryWithPagination(
          commonMintAddressesQuery,
          {
            user,
            tokenType,
            chain,
            limit,
          }
        );
      }
      // 1. Fetch all minted tokens by `user`
      const {
        data: mintedTokensData,
        error: mintedTokensError,
        hasNextPage: mintedTokensHasNextPage,
        getNextPage: mintedTokensGetNextPage,
      } = mintedTokensDataResponse ?? {};
      if (!mintedTokensError) {
        const mintedTokens = formatUserMints(mintedTokensData);
        if (mintedTokens.length === 0) break;
        for (let token of mintedTokens ?? []) {
          let commonMintersDataResponse;
          while (true) {
            if (!commonMintersDataResponse) {
              commonMintersDataResponse = await fetchQueryWithPagination(
                commonMintersQuery,
                {
                  mintedTokens: token,
                  chain,
                  limit,
                }
              );
            }
            // 2. Fetch all users that minted the same token minted by 'user'
            const {
              data: commonMintersData,
              error: commonMintersError,
              hasNextPage: commonMintersHasNextPage,
              getNextPage: commonMintersGetNextPage,
            } = commonMintersDataResponse;
            if (!commonMintersError) {
              const mint =
                commonMintersData?.TokenTransfers?.TokenTransfer?.[0];
              const { tokenAddress } = mint ?? {};
              delete mint?.operator;
              delete mint?.to;
              const mintsIndex = mintsData.findIndex(
                ({ tokenAddress: address }) => address === tokenAddress
              );
              const commonMintersList = (
                commonMintersData?.TokenTransfers?.TokenTransfer ?? []
              )
                ?.map(({ operator, to }) =>
                  operator?.addresses?.[0] === to?.addresses?.[0]
                    ? operator?.addresses?.[0]
                    : null
                )
                .filter(Boolean);
              if (commonMintersList.length !== 0) {
                if (mintsIndex === -1) {
                  mintsData = [
                    ...mintsData,
                    {
                      ...mint,
                      chain,
                      minters: commonMintersList?.filter((value, index) => {
                        return commonMintersList.indexOf(value) === index;
                      }),
                    },
                  ];
                } else {
                  mintsData[mintsIndex] = {
                    ...mintsData[mintsIndex],
                    minters: [
                      ...mintsData[mintsIndex]?.minters,
                      ...commonMintersList,
                    ].filter((value, index) => {
                      return (
                        [
                          ...mintsData[mintsIndex]?.minters,
                          ...commonMintersList,
                        ].indexOf(value) === index
                      );
                    }),
                  };
                }
              }

              if (!commonMintersHasNextPage) {
                break;
              } else {
                commonMintersDataResponse = await commonMintersGetNextPage();
              }
            } else {
              console.error("Error: ", commonMintersError);
              break;
            }
          }

          // reset common minters data for loop
          commonMintersDataResponse = null;
        }

        if (!mintedTokensHasNextPage) {
          break;
        } else {
          mintedTokensDataResponse = await mintedTokensGetNextPage();
        }
      } else {
        console.error("Error: ", mintedTokensError);
        break;
      }
    }

    // reset minted tokens data for loop
    mintedTokensDataResponse = null;
  }

  return mintsData;
};

export default fetchCommonMinters;

When the result from fetchCommonMinters is logged, it will looks as follows:

[
  {
    "tokenAddress": "0x2a9ea02e4c2dcd56ba20628fe1bd46bae2c62746",
    "token": { "name": "FarCon 2023 Tickets" },
    "chain": "ethereum",
    "minters": [
      "0xae2586e76c8a4d8dc1ff3d9ab70bec760ae143c2",
      "0x2152ad70e4b395169923e2c6e8b09cd81b50c498",
      "0xb8786d48c23bf7e5a0eff3089ba439d8e2fa6fe0"
    ]
  },
  {
    "tokenAddress": "0xd3b4de0d85c44c57993b3b18d42b00de81809eea",
    "token": { "name": "Unveiling Airstack's Onchain Graph" },
    "chain": "base",
    "minters": ["0x427a1c6dcaad92f886020a61e0b85be8e1c5ead5"]
  }
  // Other common minters and token details that they minted
]

Once the list of common minters is fetched and categorized by the tokens minted, you can then use the list of common minters 0x address as an input to fetch tokens that they are minting within the given interval:

index.ts
import { init, fetchQueryWithPagination } from "@airstack/node";
import { config } from "dotenv";
import { interval, tokenType, chains, limit } from "./constant";
import fetchCommonMinters from "./functions/fetchCommonMinters";
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,
  $commonMinter: Identity
) {
  TokenTransfers(
    input: {
      filter: {
        from: {_eq: "0x0000000000000000000000000000000000000000"},
        blockTimestamp: {_gte: $startTime, _lte: $endTime},
        tokenType: {_in: $tokenType},
        formattedAmount: {_gt: 0},
        to: {_eq: $commonMinter},
        operator: {_eq: $commonMinter},
      }
      blockchain: $chain,
      order: {blockTimestamp: DESC},
      limit: $limit
    }
  ) {
    TokenTransfer {
      tokenAddress
      token {
        name
      }
    }
  }
}
`;

const main = async (user: string, currentTime: Dayjs) => {
  let response;
  let mintsData = [];
  const commonMintersByMintedTokens = await fetchCommonMinters(user);
  // Iterate over all onchain graph users
  for (let commonMinterDetail of commonMintersByMintedTokens) {
    const { tokenAddress, token, minters } = commonMinterDetail ?? {};
    for (let commonMinter of minters) {
      // 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,
              commonMinter,
            });
          }

          const { data, error, getNextPage, hasNextPage } = response ?? {};
          if (!error) {
            for (let res of data?.TokenTransfers?.TokenTransfer ?? []) {
              const mintsIndex = mintsData.findIndex(
                ({ tokenAddress: address }) => address === res?.tokenAddress
              );
              if (mintsIndex !== -1) {
                const associatedMintsIndex = mintsData[
                  mintsIndex
                ]?.associatedMints?.findIndex(
                  ({ tokenAddress: address }) => address === tokenAddress
                );
                if (associatedMintsIndex !== -1) {
                  mintsData[mintsIndex].associatedMints[
                    associatedMintsIndex
                  ].minters = [
                    ...mintsData[mintsIndex].associatedMints[
                      associatedMintsIndex
                    ].minters,
                    commonMinter,
                  ];
                  mintsData[mintsIndex].associatedMints[
                    associatedMintsIndex
                  ].frequency += 1;
                } else {
                  mintsData[mintsIndex].associatedMints = [
                    ...mintsData[mintsIndex].associatedMints,
                    {
                      tokenAddress,
                      token,
                      minters: [commonMinter],
                      frequency: 1,
                    },
                  ];
                }
              } else {
                mintsData = [
                  ...mintsData,
                  {
                    ...res,
                    chain,
                    /*
                     * associated mints is the tokens that were minted in the past
                     * that is commonly minted by the given `user`. This is kept track
                     * in order to recommend user minted tokens that were minted by
                     * the same group of users that minted certain collections
                     *
                     * For example, minters of X also minted Y
                     */
                    associatedMints: [
                      {
                        tokenAddress,
                        token,
                        minters: [commonMinter],
                        frequency: 1,
                      },
                    ],
                  },
                ];
              }
            }
            // 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;

In this step, you'll also be keeping track of associated minted tokens to the tokens minted at the given interval. This is done such that you can know which token Y that is currently minted by the list of common minters that also minted token X the past.

In addition to the associated minted token addresses, you'll also be tracking the list of 0x addresses of the minters and the number of times (frequency) that token Y has been minted by the group of minters that also minted token X.

If you log the main function, it will return a response as follows:

[
  {
    "tokenAddress": "0xad08067c7d3d3dbc14a9df8d671ff2565fc5a1ae",
    "token": { "name": "NFT.TD" },
    "chain": "ethereum",
    "associatedMints": [
      {
        "tokenAddress": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D",
        "token": { "name": "BoredApeYachtClub" },
        "minters": [
          "0xf0bf94342c1c77c17575ac72b7c10d03c8e23d8a",
          "0xd50963c3f3dce3bf7449116062a6b3234240a366",
          "0xc38c8027fa54bfc3e8f093a7994c3c5f43f416fe"
        ],
        "frequency": 5
      }
    ]
  },
  {
    "tokenAddress": "0xb63056fc3dab4f755d4d0380cf36e67c1164da64",
    "token": { "name": "Polysapien Open Edition" },
    "chain": "base",
    "associatedMints": [
      {
        "tokenAddress": "0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401",
        "token": { "name": "NameWrapper" },
        "minters": [
          "0x09ce2896b24e60cb3de34e2b826c2ef4545a7566",
          "0x95b7ce09add500052386318863d166326220d8e9",
          "0x575f10e1fe5f1ffa9d8b888b55bf59c7b8c01fa0",
          "0xf0bf94342c1c77c17575ac72b7c10d03c8e23d8a",
          "0xd50963c3f3dce3bf7449116062a6b3234240a366",
          "0xc38c8027fa54bfc3e8f093a7994c3c5f43f416fe",
          "0x9f06bbd82b7a26272d1eafa118cda2819b22d793",
          "0xeab05f8e538982516d119361d7e6c31e8fb6f7c8",
          "0x1d9a9b5e73259bbf0272c5228330ffd24bca82c0",
          "0x8135b0dd3eb53c1c391f4b228824ea60291793c9"
        ],
        "frequency": 10
      }
      // Other associated token mints, if any
    ]
  }
  // Other minted tokens data
]

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, you'll define the scoring function for a minted token to be the sum of associated token's frequency value and the length of minters array 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
import { TokenTransfer } from "./format";

/**
 * @description
 * Score all recent token mint data
 *
 * @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, associatedMints } = val ?? {};
    const valIndex = trendingMints.findIndex((value) => {
      return value?.tokenAddress === tokenAddress && value?.chain === chain;
    });
    const addScore = associatedMints
      .map(({ minters, frequency }) => minters?.length * frequency)
      .reduce((a, b) => a + b, 0);
    if (valIndex !== -1) {
      trendingMints[valIndex] = {
        ...trendingMints[valIndex],
        // For each new mints, add score of 1
        score: trendingMints[valIndex]?.score + addScore,
      };
    } else {
      // For new token mints, assigned initial score of 1
      trendingMints.push({ ...val, score: addScore });
    }
  });

  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": "0xad08067c7d3d3dbc14a9df8d671ff2565fc5a1ae",
    "token": { "name": "NFT.TD" },
    "chain": "ethereum",
    "associatedMints": [
      {
        "tokenAddress": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D",
        "token": { "name": "BoredApeYachtClub" },
        "minters": [
          "0xf0bf94342c1c77c17575ac72b7c10d03c8e23d8a",
          "0xd50963c3f3dce3bf7449116062a6b3234240a366",
          "0xc38c8027fa54bfc3e8f093a7994c3c5f43f416fe"
        ],
        "frequency": 5
      }
    ],
    "score": 15 // 5 * 3 (minters?.length)
  },
  {
    "tokenAddress": "0xb63056fc3dab4f755d4d0380cf36e67c1164da64",
    "token": { "name": "Polysapien Open Edition" },
    "chain": "base",
    "associatedMints": [
      {
        "tokenAddress": "0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401",
        "token": { "name": "NameWrapper" },
        "minters": [
          "0x09ce2896b24e60cb3de34e2b826c2ef4545a7566",
          "0x95b7ce09add500052386318863d166326220d8e9",
          "0x575f10e1fe5f1ffa9d8b888b55bf59c7b8c01fa0",
          "0xf0bf94342c1c77c17575ac72b7c10d03c8e23d8a",
          "0xd50963c3f3dce3bf7449116062a6b3234240a366",
          "0xc38c8027fa54bfc3e8f093a7994c3c5f43f416fe",
          "0x9f06bbd82b7a26272d1eafa118cda2819b22d793",
          "0xeab05f8e538982516d119361d7e6c31e8fb6f7c8",
          "0x1d9a9b5e73259bbf0272c5228330ffd24bca82c0",
          "0x8135b0dd3eb53c1c391f4b228824ea60291793c9"
        ],
        "frequency": 10
      }
      // Other associated token mints, if any
    ],
    "score": 100 // 10 * 10 (minters?.length)
  }
  // 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 { TokenTransfer } from "./format";

export interface TokenTransferWithScore extends TokenTransfer {
  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": "0xb63056fc3dab4f755d4d0380cf36e67c1164da64",
    "token": { "name": "Polysapien Open Edition" },
    "chain": "base",
    "associatedMints": [
      {
        "tokenAddress": "0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401",
        "token": { "name": "NameWrapper" },
        "minters": [
          "0x09ce2896b24e60cb3de34e2b826c2ef4545a7566",
          "0x95b7ce09add500052386318863d166326220d8e9",
          "0x575f10e1fe5f1ffa9d8b888b55bf59c7b8c01fa0",
          "0xf0bf94342c1c77c17575ac72b7c10d03c8e23d8a",
          "0xd50963c3f3dce3bf7449116062a6b3234240a366",
          "0xc38c8027fa54bfc3e8f093a7994c3c5f43f416fe",
          "0x9f06bbd82b7a26272d1eafa118cda2819b22d793",
          "0xeab05f8e538982516d119361d7e6c31e8fb6f7c8",
          "0x1d9a9b5e73259bbf0272c5228330ffd24bca82c0",
          "0x8135b0dd3eb53c1c391f4b228824ea60291793c9"
        ],
        "frequency": 10
      }
      // Other associated token mints, if any
    ],
    "score": 100
  },
  {
    "tokenAddress": "0xad08067c7d3d3dbc14a9df8d671ff2565fc5a1ae",
    "token": { "name": "NFT.TD" },
    "chain": "ethereum",
    "associatedMints": [
      {
        "tokenAddress": "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D",
        "token": { "name": "BoredApeYachtClub" },
        "minters": [
          "0xf0bf94342c1c77c17575ac72b7c10d03c8e23d8a",
          "0xd50963c3f3dce3bf7449116062a6b3234240a366",
          "0xc38c8027fa54bfc3e8f093a7994c3c5f43f416fe"
        ],
        "frequency": 5
      }
    ],
    "score": 15
  }
  // Other scored and sorted token mints data
]

Filtering

As the last step, you can then filter the scored and sorted mints data to determine which one would qualify as "trending mints" to be notified/shown to the user.

In this tutorial, you'll be using a very simple filtering function filterFunction that will filter out any result that have score below or equal to the threshold variable that you can set for the user:

utils/filter.ts
import { TokenTransferWithScore } from "./scoring";

/**
 * @description
 * Filter mints data by `score` field that reaches
 * certain `threshold` that would classify as trending mints
 *
 * @example
 * const res = filterFunction(sortedData, 50);
 *
 * @param {Object} data – Scored & sorted tokens data with `score` field
 * @returns list of trending mints
 */
const filterFunction = (data: TokenTransferWithScore, threshold: number) =>
  data?.filter((val) => val?.score >= threshold);

export default filterFunction;

Then, you can import the filterFunction back to main to have the sorted and scored data filtered:

index.ts
// same imports as above
import filterFunction from "./utils/filter";

const main = (user: string, currentTime: Dayjs) = > {
  // same as above
  const filteredData = filterFunction(sortedData, 50); // Only output result with score above 50
  return filteredData;
}

export default main;

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

[
  {
    "tokenAddress": "0xb63056fc3dab4f755d4d0380cf36e67c1164da64",
    "token": { "name": "Polysapien Open Edition" },
    "chain": "base",
    "associatedMints": [
      {
        "tokenAddress": "0xd4416b13d2b3a9abae7acd5d6c2bbdbe25686401",
        "token": { "name": "NameWrapper" },
        "minters": [
          "0x09ce2896b24e60cb3de34e2b826c2ef4545a7566",
          "0x95b7ce09add500052386318863d166326220d8e9",
          "0x575f10e1fe5f1ffa9d8b888b55bf59c7b8c01fa0",
          "0xf0bf94342c1c77c17575ac72b7c10d03c8e23d8a",
          "0xd50963c3f3dce3bf7449116062a6b3234240a366",
          "0xc38c8027fa54bfc3e8f093a7994c3c5f43f416fe",
          "0x9f06bbd82b7a26272d1eafa118cda2819b22d793",
          "0xeab05f8e538982516d119361d7e6c31e8fb6f7c8",
          "0x1d9a9b5e73259bbf0272c5228330ffd24bca82c0",
          "0x8135b0dd3eb53c1c391f4b228824ea60291793c9"
        ],
        "frequency": 10
      }
      // Other associated token mints, if any
    ],
    "score": 100
  }
]

Step 3: Run as a Cronjob

With the code from above, now you can run this periodically non stop as a cron to fetch all the recent trending mints to be notified or recommended to your user.

From your user perspective, they will experience the feature in either user interface or push notifications.

User Interface

For displaying all the trending token mints to your interface, it is best practice that you store the recent trending mints data from in your preferred database.

cron.ts
import cron from "node-cron";
import dayjs from "dayjs";
import main from "./main";

cron.schedule("0 * * * *", async () => {
  const currentTime = dayjs();
  const data = await main(user, currentTime);
  // Store `data` to your preferred DB
});

From there, you can fetch trending mints data from database directly through your application's frontend or backend to be served to your users' client.

Push Notifications

For push notification, you simply need to push the message to your client using the information fetched from the Airstack API through the cron and will not require any additional storage:

cron.ts
import cron from "node-cron";
import dayjs from "dayjs";
import main from "./main";
import { interval } from "./constant";

cron.schedule("0 * * * *", () => {
  const currentTime = dayjs();
  const [
    trendingMint1,
    trendingMint2,
    // Other trending mints in the array
  ] = await main(user, currentTime);
  const { token, associatedMints, score } = trendingMint1 ?? {};
  const { minters, token: associatedToken } = associatedMints?.[0];
  const message = `${minters?.length} user that minted ${associatedToken?.name} before have now also minted ${token?.name} in the last ${interval} minutes`;
  // Here make API call with `message` to the push service to notify
  // your app's client
});

Developer Support

🎉 đŸĨŗ Congratulations you've just integrated trending mints feature based on your user's common minters into your application!

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

More Resources

Last updated

Was this helpful?