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.
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:
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
Code
queryMyQuery( $startTime: Time, $endTime: Time, $tokenType: [TokenType!], $chain: TokenBlockchain!, $limit: Int, $onchainGraphUser: Identity) { TokenTransfers( input: {filter: { # Only get token transfers that are mintsfrom: {_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 } } }}
{"data": {"TokenTransfers": {"TokenTransfer": [ {// The address of minted NFT"tokenAddress":"0xd3b4de0d85c44c57993b3b18d42b00de81809eea","token": {"name":"Unveiling Airstack's Onchain Graph" } } ] } }}
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);constquery=`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 } } }}`;constmain=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. */constonchainGraphUsers= [ { address:"0xb59aa5bb9270d44be3fa9b6d67520a2d28cf80ab", onchainScore:96, }, { address:"0xf6b6f07862a02c85628b3a9688beae07fea9c863", onchainScore:64, }, { address:"0xcbfbcbfca74955b8ab75dec41f7b9ef36f329879", onchainScore:60, },// Other onchain graph users ];// Iterate over all onchain graph usersfor (let onchainGraphUser of onchainGraphUsers) {const { address,onchainScore } = onchainGraphUser ?? {};// Iterate over all blockchainfor (let chain of chains) {while (true) {if (!response) { response =awaitfetchQueryWithPagination(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 paginationsif (!hasNextPage) {break; } else { response =awaitgetNextPage(); } } else {console.error("Error: ", error);break; } }// Resetting the loop response =null; } }return mintsData;}exportdefault main;
index.js
import { init, fetchQueryWithPagination } from"@airstack/node";import { config } from"dotenv";config();init(process.env.AIRSTACK_API_KEY);constquery=`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 } } }}`;constmain=async (user, currentTime) => {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. */constonchainGraphUsers= [ { address:"0xb59aa5bb9270d44be3fa9b6d67520a2d28cf80ab", onchainScore:96, }, { address:"0xf6b6f07862a02c85628b3a9688beae07fea9c863", onchainScore:64, }, { address:"0xcbfbcbfca74955b8ab75dec41f7b9ef36f329879", onchainScore:60, },// Other onchain graph users ];// Iterate over all onchain graph usersfor (let onchainGraphUser of onchainGraphUsers) {const { address,onchainScore } = onchainGraphUser ?? {};// Iterate over all blockchainfor (let chain of chains) {while (true) {if (!response) { response =awaitfetchQueryWithPagination(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) { mintsData = [...mintsData,...(data?.TokenTransfers?.TokenTransfer?.map((mint) => ({...mint, minter: { address, onchainScore, }, chain, })) ?? []), ];// Iterate over all paginationsif (!hasNextPage) {break; } else { response =awaitgetNextPage(); } } else {console.error("Error: ", error);break; } }// Resetting the loop response =null; } }return mintsData;};exportdefault main;
index.py
import osfrom airstack.execute_query import AirstackClientfrom dotenv import load_dotenvfrom constant import interval, token_type, chains, limitfrom utils.format import format_functionfrom datetime import datetime, timedeltafrom typing import List, Dict, Anyload_dotenv()api_key = os.environ.get("AIRSTACK_API_KEY")api_client =AirstackClient(api_key=api_key)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 } } }}"""asyncdefmain(user:str,current_time: datetime) -> List[Dict[str, Any]]: query_response =None mints_data = []""" 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. """ onchainGraphUsers = [{ address:"0xb59aa5bb9270d44be3fa9b6d67520a2d28cf80ab", onchainScore:96,},{ address:"0xf6b6f07862a02c85628b3a9688beae07fea9c863", onchainScore:64,},{ address:"0xcbfbcbfca74955b8ab75dec41f7b9ef36f329879", onchainScore:60,},# Other onchain graph users ];# Iterate over all on chain graph usersfor onchainGraphUser in onchainGraphUsers:# Iterate over all blockchainfor chain in chains:whileTrue:if query_response isNone: execute_query_client = api_client.create_execute_query_object( query=query, variables={"startTime": (current_time +timedelta(hours=-interval)).strftime("%Y-%m-%dT%H:%M:%SZ"),"endTime": current_time.strftime("%Y-%m-%dT%H:%M:%SZ"),"tokenType": token_type,"chain": chain,"limit": limit,"onchainGraphUser": onchainGraphUser.get("address", "") }) query_response =await execute_query_client.execute_paginated_query()if query_response.error isNone: data = query_response.dataif data and'TokenTransfers'in data and data['TokenTransfers']isnotNone: token_transfers = data['TokenTransfers'].get('TokenTransfer')if token_transfers:for mint in token_transfers: new_mint = mint.copy() new_mint['minter']={'address': onchainGraphUser.get("address", ""),'onchainScore': onchainGraphUser.get("onchainScore", "")} new_mint['chain']= chain mints_data.append(new_mint)# Iterate over all paginationsifnot query_response.has_next_page:breakelse: query_response =await query_response.get_next_page# Resetting the loop query_response =Nonereturn mints_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:
Scoring â assigning a score to each minted tokens to determine how "popular" each tokens are in a certain period of time
Sorting â sort the minted tokens data by the score, in descending order
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
exportinterfaceData { TokenTransfers:TokenTransfer;}exportinterfaceTokenTransfer { TokenTransfer:TokenTransferDetails[];}exportinterfaceTokenTransferDetails { tokenAddress:string; operator:Identity; to:Identity; token:Token;}exportinterfaceIdentity { addresses:string;}exportinterfaceToken { 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 */constscoringFunction= (data:TokenTransfer[]) => {let trendingMints = [];data.forEach((val) => {const { tokenAddress,chain,minter } = val ?? {};const { address,onchainScore } = minter ?? {};constvalIndex=trendingMints.findIndex((value) => {returnvalue?.tokenAddress === tokenAddress &&value?.chain === chain; });if (valIndex !==-1) {constminterIndex= 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 {deleteval?.minter;// For new token mints, assigned initial score of the minter's onchain scoretrendingMints.push({ ...val, score: onchainScore, minters: [minter] }); } });return trendingMints;};exportdefault scoringFunction;
utils/scoring.js
/** * @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 */constscoringFunction= (data) => {let trendingMints = [];data.forEach((val) => {const { tokenAddress,chain,minter } = val ?? {};const { address,onchainScore } = minter ?? {};constvalIndex=trendingMints.findIndex((value) => {returnvalue?.tokenAddress === tokenAddress &&value?.chain === chain; });if (valIndex !==-1) {constminterIndex= 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 {deleteval?.minter;// For new token mints, assigned initial score of the minter's onchain scoretrendingMints.push({ ...val, score: onchainScore, minters: [minter] }); } });return trendingMints;};exportdefault scoringFunction;
utils/scoring.py
defscoring_function(data):""" Score all recent token mint data by how much people mint the token. Each minting represents a score of 1. :param data: Formatted minted tokens data from Airstack API :return: Scored mint data """ trending_mints = []for val in data: token_address = val.get('tokenAddress') chain = val.get('chain') minter = val.get('minter', {}) address = minter.get('address') onchain_score = minter.get('onchainScore', 0) val_index =next((index for (index, value) inenumerate(trending_mints)if value.get('tokenAddress') == token_address and value.get('chain') == chain), -1)if val_index !=-1: minter_index =next((index for (index, m) inenumerate(trending_mints[val_index].get('minters', []))if m.get('address') == address and m.get('onchainScore') == onchain_score), -1) trending_mints[val_index]['score'] += onchain_scoreif minter_index ==-1: trending_mints[val_index]['minters'].append(minter)else: val.pop('minter', None) trending_mints.append({**val, 'score': onchain_score, 'minters': [minter]})# Filter out None values in mintersfor item in trending_mints: item['minters']= [m for m in item['minters']if m]return trending_mints
Then, you can import the scoringFunction back to main to have the data from Airstack API scored:
index.ts
// same imports as aboveimport scoringFunction from"./utils/scoring";constmain= (user:string, currentTime:Dayjs) = > {// same as above const scoredData =scoringFunction(mintsData); return scoredData}exportdefault main;
index.js
// same imports as aboveimport { scoringFunction } from"./utils/scoring";constmain= (currentTime) => {// same as above const scoredData =scoringFunction(mintsData); return scoredData}exportdefault main;
index.py
# same imports as abovefrom utils.scoring import scoring_functionasyncdefmain(current_time: datetime) -> List[Dict[str, Any]]:# same as above scored_data =scoring_function(mints_data)return scored_data
When the result of the newly modified main function is logged, it will have result that look as follows:
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";exportinterfaceTokenTransferWithScoreextendsTokenTransferDetails { 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 */constsortingFunction= (scoredData:TokenTransferWithScore) =>scoredData?.sort((a, b) =>b.score -a.score);exportdefault sortingFunction;
utils/sorting.js
/** * @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 */constsortingFunction= (scoredData) =>scoredData?.sort((a, b) =>b.score -a.score);exportdefault sortingFunction;
Then, you can import the sortingFunction back to main to have the scored data sorted:
index.ts
// same imports as aboveimport { sortingFunction } from"./utils/sorting";constmain= (user:string, currentTime:Dayjs) = > {// same as above const sortedData =sortingFunction(scoredData); return sortedData}exportdefault main;
index.js
// same imports as aboveimport sortingFunction from"./utils/sorting";constmain= () => {// same as above const sortedData =sortingFunction(scoredData); return sortedData}exportdefault main;
index.py
# same imports as abovefrom utils.sorting import sorting_functionasyncdefmain(current_time: datetime) -> List[Dict[str, Any]]:# same as above sorted_data =sorting_function(scored_data)return sorted_data
When the result of the newly modified main function is logged, it will have result that look as follows:
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 */constfilterFunction= (data:TokenTransferWithScore, threshold:number) =>data?.filter((val) =>val?.score >= threshold);exportdefault filterFunction;
utils/fitler.js
/** * @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 */constfilterFunction= (data, threshold) =>data?.filter((val) =>val?.score >= threshold);exportdefault filterFunction;
utils/filter.py
from typing import List, Dict, Anydeffilter_function(data: List[Dict[str, Any]],threshold:int) -> List[Dict[str, Any]]:""" Filter mints data by `score` field that reaches a certain `threshold` that would classify as trending mints. :param data: Scored & sorted tokens data with `score` field :param threshold: The threshold score to filter the trending mints :return: List of trending mints """if data isNone:return []return [val for val in data if val.get('score', 0)>= threshold]
Then, you can import the filterFunction back to main to have the sorted and scored data filtered:
index.ts
// same imports as aboveimport filterFunction from"./utils/filter";constmain= (user:string, currentTime:Dayjs) = > {// same as above const filteredData =filterFunction(sortedData,50); // Only output result with score above 50 return filteredData;}exportdefault main;
// same imports as aboveimport filterFunction from"./utils/filter";constmain= () => {// same as above const filteredData =filterFunction(sortedData); // filter only result above 50 return filteredData;}exportdefault main;
index.py
# same imports as abovefrom utils.filter import filter_functionasyncdefmain(current_time: datetime) -> List[Dict[str, Any]]:# same as above filtered_data =filter_function(sorted_data, 50)# Only output result with score above 50return filtered_data
When the result of the newly modified main function is logged, it will have result that look as follows:
[ {"tokenAddress":"0xc347075b60ff7f07eea970636ea9a8f95d7e7da9","token": { "name":"Shadowink" },"chain":"ethereum","score":510,"minters": [ {"address":"0xc94327cbb9eef801a4bbe3459766bb712c5ad979","onchainScore":51 } ] }, {"tokenAddress":"0xdda213a564ec3ba7a6e82c529ddd1aa37b4d0fb4","token": { "name":"Moonie Punks" },"chain":"ethereum","score":459,"minters": [ {"address":"0xc94327cbb9eef801a4bbe3459766bb712c5ad979","onchainScore":51 } ] }// Other scored, sorted, and filtered token mints data by onchain graph users]
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 () => {constcurrentTime=dayjs();constdata=awaitmain(user, currentTime);// Store `data` to your preferred DB});
cron.js
import cron from"node-cron";import dayjs from"dayjs";import main from"./main";cron.schedule("0 * * * *",async () => {constcurrentTime=dayjs();constdata=awaitmain(user, currentTime);// Store `data` to your preferred DB});
cron.py
import pycronfrom datetime import datetimefrom index import main@pycron.cron("* * * * *")asyncdefcron(current_time: datetime):print("Running cron job...") data =awaitmain(user, current_time)# Store `data` to your preferred DBif__name__=='__main__':print("Starting cron job...") pycron.start()
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 * * * *", () => {constcurrentTime=dayjs();const [trendingMint1,trendingMint2,// Other trending mints in the array ] =awaitmain(user, currentTime);const { token,minters,score } = trendingMint1 ?? {};constmessage=`${minters?.[0]?.address} and ${minters?.length-2} have minted ${token?.name} in the last ${interval} minutes`;// Here make API call with `message` to the push service to notify// your app's client});
cron.js
import cron from"node-cron";import dayjs from"dayjs";import main from"./main";import { interval } from"./constant";cron.schedule('0 * * * *', () => {constcurrentTime=dayjs();const [trendingMint1,trendingMint2,// Other trending mints in the array ] =awaitmain(user, currentTime);const { token,score } = trendingMint1 ?? {};constmessage=`${minters?.[0]?.address} and ${minters?.length-2} have minted ${token?.name} in the last ${interval} minutes`;// Here make API call with `message` to the push service to notify// your app's client});
cron.py
import pycronfrom datetime import datetimefrom index import main@pycron.cron("* * * * *")asyncdefcron(current_time: datetime):print("Running cron job...") data =awaitmain(user, current_time) token = data[0].get("token", {}) score = data[0].get("score", 0) message =f"{token.get('name', '')} have been minted more than {score} times the last {interval} minutes";# Here make API call with `message` to the push service to notify# your app's clientif__name__=='__main__':print("Starting cron job...") pycron.start()
Developer Support
đ đĨŗ Congratulations you've just integrated trending mints feature based on your user's onchain graph 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.