πŸ‘›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;