Recommendations

As the NFT landscape continues to evolve, offering personalized recommendations can significantly enhance user engagement, improve asset discovery, and ultimately drive transaction volume. This guide serves as a roadmap, detailing various methodologies that you, as a developer, can adopt to deliver curated, relevant, and dynamic NFT suggestions to your users.

Whether you're aiming to base your recommendations on user activity, similarity between NFTs or collections, or predictive models, our APIs provide the flexibility and robustness necessary to cater to all these requirements.

We explore multiple approaches including wallet behavioral categories, collection and holding similarities, ownership overlap, and hybrid methods, each with their own strengths and nuances. This guide is designed to equip you with the knowledge and resources to create a compelling, personalized experience that will set your crypto wallet or marketplace apart.

The following guide is based on the predictive wallet behaviors, specifically, there are two behavior categories that we are exploring in this guide:

CategoryDescription
CollectorA wallet that is more likely to collect and hold, has a high purchase intent in the next 1-3 months, and is unlikely to quickly flip an NFT for short term profit.
NewbieWallet that has a very low experience, taste and automation scores and seems to be fairly new to the space, low likelihood to purchase or sell.

Goals of the recommender:

  • Activate the newbies and convert them into collectors;
  • Activate the explorers and convert them into collectors;

You can play around with other behavior categories and decide which strategy is most relevant for your use case.

Top collections attracting the most newbies or collectors

The approach is to identify collections that have amassed the most newbies or collectors at all time. It is a simple and quick way to recommend time-tested collections to activate newbies.

Use Collections by Behavior endpoint and apply filter BEHAVIOR_NEWBIE or BEHAVIOR_COLLECTOR.

curl --request GET \
     --url https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/top_collections/by_behavior/BEHAVIOR_NEWBIE \
     --header 'X-API-Key: <YOUR API KEY>' \
     --header 'accept: application/json'

The results are sorted by the number of wallets that own the collection with a given behavior:

{
  "collections": [
    {
      "contractAddress": "0x7e789e2dd1340971de0a9bca35b14ac0939aa330",
      "name": "Crypto stamp Edition 1",
      "ownersCountMatched": "148193",
      "ownersCountTotal": "148887"
    },
    {
      "contractAddress": "0xd4307e0acd12cf46fd6cf93bc264f5d5d1598792",
      "name": "Base, Introduced",
      "ownersCountMatched": "131384",
      "ownersCountTotal": "365625"
    },
    {
      "contractAddress": "0x929832b1f1515cf02c9548a0ff454f1b0e216b18",
      "name": "HyperNFT_HOS_1.0",
      "ownersCountMatched": "103500",
      "ownersCountTotal": "111435"
    },
    {
      "contractAddress": "0x5c891d76584b46bc7f1e700169a76569bb77d2db",
      "name": "OKXFootballCup",
      "ownersCountMatched": "82764",
      "ownersCountTotal": "97461"
    },
    {
      "contractAddress": "0xfaafdc07907ff5120a76b34b731b278c38d6043c",
      "name": "Enjin",
      "ownersCountMatched": "69546",
      "ownersCountTotal": "134628"
    },
    {
      "contractAddress": "0x05dbbe4baed86d9b1da83e67dea6326e2617dad2",
      "name": "LucidSight-DODGERS-NFT",
      "ownersCountMatched": "39232",
      "ownersCountTotal": "39606"
    },
    {
      "contractAddress": "0x7492e30d60d96c58ed0f0dc2fe536098c620c4c0",
      "name": "Rainbow Zorb",
      "ownersCountMatched": "31890",
      "ownersCountTotal": "123443"
    },
    {
      "contractAddress": "0xbd13e53255ef917da7557db1b7d2d5c38a2efe24",
      "name": "DozerDoll",
      "ownersCountMatched": "25467",
      "ownersCountTotal": "58904"
    },
    {
      "contractAddress": "0xe42cad6fc883877a76a26a16ed92444ab177e306",
      "name": "TheMerge",
      "ownersCountMatched": "22656",
      "ownersCountTotal": "142888"
    },
    {
      "contractAddress": "0xd1e5b0ff1287aa9f9a268759062e4ab08b9dacbe",
      "name": ".crypto",
      "ownersCountMatched": "17863",
      "ownersCountTotal": "69560"
    }
  ]
}

Trending collections among newbies or collectors

This approach is similar to the approach we used above, however, instead of basing our recommendations on time-tested collections we will use time-relevant collections as we are analyzing specific trends within behavior categories.

The approach is to identify collections with the highest growth in newbies or the highest growth in collectors over the last 30 days.

Use Collections by Behavior Trend endpoint and filter by BEHAVIOR_NEWBIE or BEHAVIOR_COLLECTOR respectively.

curl --request GET \
     --url 'https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/top_collections/by_behavior/BEHAVIOR_NEWBIE/trend?limit=10' \
     --header 'X-API-Key: <YOUR API KEY>'

The results are ordered by the change in the number of wallets that own the collection with a given behavior over the last 30 days:

{
  "collections": [
    {
      "contractAddress": "0x0000000000001b84b1cb32787b0d64758d019317",
      "name": "HomeWork 🏠🛠️",
      "ownersCountMatched": "1",
      "ownersCountChanged": "0",
      "ownersCountTotal": "26"
    },
    {
      "contractAddress": "0x000000000000e2bff3fb88407a604cdbb5e5dd5c",
      "name": "Sidekick Early Access",
      "ownersCountMatched": "2",
      "ownersCountChanged": "0",
      "ownersCountTotal": "13"
    },
    {
      "contractAddress": "0x00000000000881d280439988781f743e8cdd1fdf",
      "name": "LANCET PASS",
      "ownersCountMatched": "46",
      "ownersCountChanged": "0",
      "ownersCountTotal": "257"
    },
    {
      "contractAddress": "0x00000000001ba87a34f0d3224286643b36646d81",
      "name": "Dungeonized",
      "ownersCountMatched": "5",
      "ownersCountChanged": "0",
      "ownersCountTotal": "1042"
    },
    {
      "contractAddress": "0x0000000000771a79d0fc7f3b7fe270eb4498f20b",
      "name": "MCT: XEN batch mint NFT",
      "ownersCountMatched": "84",
      "ownersCountChanged": "0",
      "ownersCountTotal": "378"
    },
    {
      "contractAddress": "0x00000000007d1996ae28eab84c973e5e558eec4f",
      "name": "IRIDION",
      "ownersCountMatched": "40",
      "ownersCountChanged": "0",
      "ownersCountTotal": "178"
    },
    {
      "contractAddress": "0x000000000437b3cce2530936156388bff5578fc3",
      "name": "My NFT",
      "ownersCountMatched": "1",
      "ownersCountChanged": "0",
      "ownersCountTotal": "26"
    },
    {
      "contractAddress": "0x0000000005756b5a03e751bd0280e3a55bc05b6e",
      "name": "Villagers of XOLO",
      "ownersCountMatched": "22",
      "ownersCountChanged": "0",
      "ownersCountTotal": "4726"
    },
    {
      "contractAddress": "0x0000000006bc8d9e5e9d436217b88de704a9f307",
      "name": "Curta",
      "ownersCountMatched": "4",
      "ownersCountChanged": "0",
      "ownersCountTotal": "126"
    },
    {
      "contractAddress": "0x0000000007927ce78919a6fbd7c200ed3eaefcec",
      "name": "DejaVox3 Crapauds",
      "ownersCountMatched": "5",
      "ownersCountChanged": "0",
      "ownersCountTotal": "49"
    }
  ]
}

Trending collections among similar collectors based on highest overlap

In this approach, we're personalizing recommendations using ownership overlap for maximal similarity. We'll target collections with the most growth among explorers overlapping with a specific wallet.

The benefit of this method is that it leverages the top 30 popular collections among specific collection owners, ranking them by the growth within the relevant behavior category.

Recommendations improve as the wallet's overlap with these collections increases.

Steps:

  1. Based on the ownership by a wallet of specific collection A, call Collections in Common endpoint to get the list of most commonly held collections among the owners of the collection A, and exclude already owned collections by the wallet. The more collections the wallet holds from this list the higher the likelihood for the recommendation to be relevant.
  2. For each collection from the list of top 30 returned on Step 1, call the Behaviors by Collection endpoint and rank the results by BEHAVIOR_COLLECTOR in descending order to get the collections with the highest growth in explorers in the last 30 days (see walletsCountChange30d value in the response).
  3. (Optional) Repeat Steps 1 and 2 for every collection that the wallet holds currently.

In the example below we used the following collections as the initial set (in a real implementation these would be the collections that the wallet owns):

CollectionAddress
Pudgy Penguins0xbd3531da5cf5857e7cfaa92426877b022e612cf8
Otherdeed for Otherside0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258
Doodles0x8a90cab2b38dba80c64b7734e58ee1db38b8992e
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"sort"
	"strconv"
)

type Collection struct {
	ContractAddress string `json:"contractAddress"`
}

type CollectionsResponse struct {
	Collections []Collection `json:"collections"`
}

type Behavior struct {
	Behavior              string `json:"behavior"`
	WalletsCountChange30d string `json:"walletsCountChange30d"`
}

type BehaviorsResponse struct {
	AggregatedBehaviors []Behavior `json:"aggregatedBehaviors"`
}

func main() {
	client := &http.Client{}

	// Add your initial addresses here.
	initialAddresses := []string{
		"0xbd3531da5cf5857e7cfaa92426877b022e612cf8", // Pudgy Penguins
		"0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258", // Otherdeed for Otherside
		"0x8a90cab2b38dba80c64b7734e58ee1db38b8992e", // Doodles
	}

	results := make(map[string]int)
	for _, address := range initialAddresses {
		req, _ := http.NewRequest("GET", fmt.Sprintf("https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/common/collections/%s", address), nil)
		req.Header.Add("X-API-Key", "<YOUR API KEY>")

		resp1, err := client.Do(req)
		if err != nil {
			panic(err)
		}

		body, err := io.ReadAll(resp1.Body)
		if err != nil {
			panic(err)
		}

		resp1.Body.Close()

		var collectionsResponse CollectionsResponse
		json.Unmarshal(body, &collectionsResponse)

		for _, collection := range collectionsResponse.Collections {
			if !contains(initialAddresses, collection.ContractAddress) {
				req, _ := http.NewRequest("GET", fmt.Sprintf("https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/behaviors/by_collection/%s", collection.ContractAddress), nil)
				req.Header.Add("X-API-Key", "<YOUR API KEY>")

				resp2, err := client.Do(req)
				if err != nil {
					panic(err)
				}

				body, err = io.ReadAll(resp2.Body)
				if err != nil {
					panic(err)
				}

				resp2.Body.Close()

				var behaviorsResponse BehaviorsResponse
				json.Unmarshal(body, &behaviorsResponse)

				for _, behavior := range behaviorsResponse.AggregatedBehaviors {
					if behavior.Behavior == "BEHAVIOR_COLLECTOR" {
						walletsCountChange30d, _ := strconv.Atoi(behavior.WalletsCountChange30d)
						results[collection.ContractAddress] = walletsCountChange30d
					}
				}
			}
		}
	}

	type kv struct {
		Key   string
		Value int
	}

	var sortedResults []kv
	for k, v := range results {
		sortedResults = append(sortedResults, kv{k, v})
	}

	sort.Slice(sortedResults, func(i, j int) bool {
		return sortedResults[i].Value > sortedResults[j].Value
	})

	for _, kv := range sortedResults {
		fmt.Printf("%s: %d\n", kv.Key, kv.Value)
	}
}

func contains(slice []string, item string) bool {
	for _, a := range slice {
		if a == item {
			return true
		}
	}
	return false
}
import requests
import json
from typing import Dict, List
from operator import itemgetter

class Collection:
    def __init__(self, contractAddress: str) -> None:
        self.contractAddress = contractAddress

class CollectionsResponse:
    def __init__(self, collections: List[Collection]) -> None:
        self.collections = collections

class Behavior:
    def __init__(self, behavior: str, walletsCountChange30d: str) -> None:
        self.behavior = behavior
        self.walletsCountChange30d = walletsCountChange30d

class BehaviorsResponse:
    def __init__(self, aggregatedBehaviors: List[Behavior]) -> None:
        self.aggregatedBehaviors = aggregatedBehaviors

def main():
    initial_addresses = [
        "0xbd3531da5cf5857e7cfaa92426877b022e612cf8",
        "0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258",
        "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
    ]

    headers = {
        'X-API-Key': '<YOUR API KEY>',
    }

    results: Dict[str, int] = {}
    for address in initial_addresses:
        resp1 = requests.get(f'https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/common/collections/{address}', headers=headers)
        collections_response = CollectionsResponse(**resp1.json())

        for collection in collections_response.collections:
            if collection.contractAddress not in initial_addresses:
                resp2 = requests.get(f'https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/behaviors/by_collection/{collection.contractAddress}', headers=headers)
                behaviors_response = BehaviorsResponse(**resp2.json())

                for behavior in behaviors_response.aggregatedBehaviors:
                    if behavior.behavior == "BEHAVIOR_COLLECTOR":
                        results[collection.contractAddress] = int(behavior.walletsCountChange30d)

    sorted_results = sorted(results.items(), key=itemgetter(1), reverse=True)

    for k, v in sorted_results:
        print(f'{k}: {v}')

if __name__ == "__main__":
    main()

const axios = require('axios');

const initialAddresses = [
    "0xbd3531da5cf5857e7cfaa92426877b022e612cf8",
    "0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258",
    "0x8a90cab2b38dba80c64b7734e58ee1db38b8992e",
];

const headers = {
    'X-API-Key': '<YOUR API KEY>',
};

let results = {};

const sortAndPrintResults = () => {
    const sortedResults = Object.entries(results).sort((a, b) => b[1] - a[1]);

    for (const [k, v] of sortedResults) {
        console.log(`${k}: ${v}`);
    }
};

const processBehaviors = async (contractAddress) => {
    const resp2 = await axios.get(`https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/behaviors/by_collection/${contractAddress}`, {headers: headers});

    for (const behavior of resp2.data.aggregatedBehaviors) {
        if (behavior.behavior === "BEHAVIOR_COLLECTOR") {
            results[contractAddress] = parseInt(behavior.walletsCountChange30d);
        }
    }
};

const processCollections = async (address) => {
    const resp1 = await axios.get(`https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/common/collections/${address}`, {headers: headers});

    const collections = resp1.data.collections;

    for (const collection of collections) {
        if (!initialAddresses.includes(collection.contractAddress)) {
            await processBehaviors(collection.contractAddress);
        }
    }
};

const main = async () => {
    for (const address of initialAddresses) {
        await processCollections(address);
    }

    sortAndPrintResults();
};

main();

The resulting list of the recommended collections:

CollectionAddressWallets Count Change 30d
ALTS by adidas0x749f5ddf5ab4c1f26f74560a78300563c34b417d1823
Otherdeed Expanded0x790b2cf29ed4f310bf7641f013c65d4560d28371784
Otherside Vessels0x5b1085136a811e55b2bb2ca1ea456ba82126a376697
Otherside Koda0xe012baf811cf9c05c408e879c399960d1f305903109
Cozy Penguins NFT0x63d48ed3f50aba950c17e37ca03356ccd6b6a2800

Trending collections among similar collectors

In this approach we use similarity to make recommendations more personal, based on the ownership of the target two collections with the goal to activate explorers and convert them into collectors.

We will identify collections with the highest growth in collectors who also hold our target A & B collections over the last 30 days.

First step

As a first step, we are going to find wallets that hold collections A & B, and with the behavior BEHAVIOR_COLLECTOR, using Segmented Audience endpoint. Optionally, we can further refine the results by filtering wallets with the certain total spend (ex: > 2 ETH) and average NFT purchase price (ex: > 1 ETH).

curl --request POST \
     --url https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/segmented_audience \
     --header 'X-API-Key: <YOUR API KEY>' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "collections": [
    {
      "contractAddress": "0xed5af388653567af2f388e6224dc7c4b3241c544",
      "nftsOwnedCountGte": "1"
    },
    {
      "contractAddress": "0x5af0d9827e0c53e4799bb226655a1de152a425a5",
      "nftsOwnedCountGte": "1"
    }
  ],
  "collectionsCondition": "CONDITION_ALL",
  "nftsPurchasesVolumeGte": "10",
  "nftsPurchasesPriceAvgGte": "1",
  "collectionsOwnedCountGte": "5",
  "behavior": "BEHAVIOR_COLLECTOR"
}
'

In the example above we used Pudgy Penguins and Otherdeed for Otherside collections.

The results are sorted by total spend in descending order:

{
  "wallets": [
    {
      "walletAddress": "0x1c051112075feaee33bcdbe0984c2bb0db53cf47",
      "totalSpend": "1274.44241791638216421199999999999999999998"
    },
    {
      "walletAddress": "0x83baf7c1b4ba0a4fcdb7c20120c60b858fda55f3",
      "totalSpend": "593.18943641237378582799999999999999999998"
    },
    {
      "walletAddress": "0x5f747cf7aa9e03dc2bfed25fa8cce89bf06488b8",
      "totalSpend": "572.62898663040317148635000000000000000002"
    },
    {
      "walletAddress": "0xc7598e5f348a33cd8d5e5ef087f59305255960c2",
      "totalSpend": "344.76680000000000000000000000000000000022"
    },
    {
      "walletAddress": "0x55800060d2d61d6d88eb60ca227ebff92ba03e2d",
      "totalSpend": "273.24334649910250399999999999999999999995"
    }
  ]
}

Next, we explore two possible methods to complete the recommender.

Method 1

For each wallet from the result above fetch the last 10-30 days of sales and compute counts for each contract_address. At this step we are looking to identify collections that were most bought in by the segmented audience from the previous step, thus we are counting the number of purchases by each wallet of any collection within a given time window.

Make sure to use NFT Transfers endpoint from the Wallet Intelligence API as this endpoint allows to filter out spam transfers. You may also want to exclude collections A and B that were used to segment the wallets in the first place when counting purchases.

Parameters used:

ParameterValueDescription
blockTimestampGtProvide a value that is between 10-30 days agoEx: 2023-04-30T14:15:22Z
labelsAnyLABEL_SALEGet only sale transfers
tokenIsSpamSPAM_FILTER_EXCLUDEExclude potential spam
partyPARTY_RECIPIENTGet only purchases
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"strconv"
)

// Define the JSON response structure
type Response struct {
	NftTransfers []struct {
		ContractAddress string `json:"contractAddress"`
	} `json:"nftTransfers"`
}

func main() {
	client := &http.Client{}
	offset := 0
	limit := 500

	// Final collections set with the total count of purchases
	// among the selected wallets.
	resultMap := make(map[string]int)

	// Set of wallets from the previous step.
	wallets := []string{
		"0x1c051112075feaee33bcdbe0984c2bb0db53cf47",
		"0x83baf7c1b4ba0a4fcdb7c20120c60b858fda55f3",
		"0x5f747cf7aa9e03dc2bfed25fa8cce89bf06488b8",
		"0xc7598e5f348a33cd8d5e5ef087f59305255960c2",
		"0x55800060d2d61d6d88eb60ca227ebff92ba03e2d",
	}

	for _, wallet := range wallets {
		log.Println("Processing wallet:", wallet)
		baseURL := fmt.Sprintf("https://ethereum-rest.api.mnemonichq.com/wallets/v1beta2/%s/nft_transfers", wallet)

		for {
			req, err := http.NewRequest("GET", baseURL, nil)
			if err != nil {
				log.Fatal(err)
			}

			// Add query parameters.
			q := req.URL.Query()
			q.Add("limit", strconv.Itoa(limit))
			q.Add("offset", strconv.Itoa(offset))
			q.Add("sortDirection", "SORT_DIRECTION_ASC")
			q.Add("blockTimestampGt", "2023-01-30T14:15:22Z")
			q.Add("labelsAny", "LABEL_SALE")
			q.Add("tokenIsSpam", "SPAM_FILTER_EXCLUDE")
			q.Add("party", "PARTY_RECIPIENT")
			req.URL.RawQuery = q.Encode()

			// Add headers
			req.Header.Add("X-API-Key", "<YOUR API KEY>")
			req.Header.Add("accept", "application/json")

			resp, err := client.Do(req)
			if err != nil {
				log.Fatal(err)
			}

			body, err := io.ReadAll(resp.Body)
			if err != nil {
				log.Fatal(err)
			}

			resp.Body.Close()

			var response Response
			if err := json.Unmarshal(body, &response); err != nil {
				log.Fatal(err)
			}

			// If no more results, break the loop.
			if len(response.NftTransfers) == 0 {
				log.Println("No more results", wallet)
				break
			}

			// Update the result map.
			for _, transfer := range response.NftTransfers {
				resultMap[transfer.ContractAddress]++
			}

			offset += limit
		}
	}

	// Print the results.
	for collection, count := range resultMap {
		fmt.Printf("Collection address: %s, Count: %d\n", collection, count)
	}
}
import requests
import json

offset = 0
limit = 500

# Final collections set with the total count of purchases
# among the selected wallets.
result_map = {}

# Set of wallets from the previous step.
wallets = [
    "0x585f4fbe2d2a889c286fa71fb81d01f30773f4b1",
    "0x34b14e83d7da6c6a30c46bf89ff93b066e139154",
    "0x93af1b9bdd5a98ba92d746dbb8217e652309c857",
    "0x9355e3fa0d4b14a14ca160497e20438249b7c360",
    "0xa5c8156188d1edf09c7df15f23513d3c6964e849"
]

for wallet in wallets:
    base_url = f"https://ethereum-rest.api.mnemonichq.com/wallets/v1beta2/{wallet}/nft_transfers"
    while True:
        # Set up query parameters and headers
        params = {
            "limit": limit,
            "offset": offset,
            "sortDirection": "SORT_DIRECTION_ASC",
            "blockTimestampGt": "2023-04-30T14%3A15%3A22Z",
            "labelsAny": "LABEL_SALE",
            "tokenIsSpam": "SPAM_FILTER_EXCLUDE",
            "party": "PARTY_RECIPIENT"
        }
        headers = {
            "X-API-Key": "<YOUR API KEY>",
            "accept": "application/json"
        }

        # Make the request and get the response
        resp = requests.get(base_url, params=params, headers=headers)
        resp.raise_for_status()

        # Parse the JSON response
        response = resp.json()

        # If no more results, break the loop
        if len(response['nftTransfers']) == 0:
            break

        # Update the result map
        for transfer in response['nftTransfers']:
            contract_address = transfer['contractAddress']
            result_map[contract_address] = result_map.get(contract_address, 0) + 1

        offset += limit

# Print the results
for collection, count in result_map.items():
    print(f"Collection address: {collection}, Count: {count}")

const axios = require('axios');
const _ = require('lodash');

let offset = 0;
const limit = 500;

let resultMap = {};

let wallets = [
    "0x585f4fbe2d2a889c286fa71fb81d01f30773f4b1",
    "0x34b14e83d7da6c6a30c46bf89ff93b066e139154",
    "0x93af1b9bdd5a98ba92d746dbb8217e652309c857",
    "0x9355e3fa0d4b14a14ca160497e20438249b7c360",
    "0xa5c8156188d1edf09c7df15f23513d3c6964e849"
];

const fetchWalletTransfers = async (wallet) => {
    const url = `https://ethereum-rest.api.mnemonichq.com/wallets/v1beta2/${wallet}/nft_transfers`;

    while (true) {
        const params = {
            limit: limit,
            offset: offset,
            sortDirection: "SORT_DIRECTION_ASC",
            blockTimestampGt: "2023-04-30T14%3A15%3A22Z",
            labelsAny: "LABEL_SALE",
            tokenIsSpam: "SPAM_FILTER_EXCLUDE",
            party: "PARTY_RECIPIENT"
        };
        const headers = {
            "X-API-Key": "<YOUR API KEY>",
            "accept": "application/json"
        };

        const response = await axios.get(url, { params: params, headers: headers });
        const transfers = response.data.nftTransfers;

        if (transfers.length === 0) {
            break;
        }

        transfers.forEach(transfer => {
            const contractAddress = transfer.contractAddress;
            resultMap[contractAddress] = resultMap[contractAddress] ? resultMap[contractAddress] + 1 : 1;
        });

        offset += limit;
    }
};

const fetchAllWalletsTransfers = async () => {
    for (const wallet of wallets) {
        await fetchWalletTransfers(wallet);
    }

    // Sort the resultMap by count of transfers in descending order
    const sortedResultMap = _.orderBy(Object.keys(resultMap).map(key => [key, resultMap[key]]), item => item[1], 'desc');

    // Print the sorted results
    sortedResultMap.forEach(item => {
        console.log(`Collection address: ${item[0]}, Count: ${item[1]}`);
    });
};

fetchAllWalletsTransfers().catch(error => console.error(error));

At this point we've got collections that were most bought in by the selected wallets in the last N days.

By executing the program above we've got the following collections that were most bought in by the given list of wallets in the last 30 days:

CollectionAddressCount
Milady Maker0x5af0d9827e0c53e4799bb226655a1de152a425a515
BEANZ Official0x306b1ea3ecdf94ab739f1910bbda052ed4a9f94917
Doodles0x8a90cab2b38dba80c64b7734e58ee1db38b8992e3
Otherdeed for Otherside0x34d85c9cdeb23fa97cb08333b511ac86e1c4e2582
Genesis Adventurers (for Loot)0x8db687aceb92c66f013e1d614137238cc698fedb2
Redacted Remilio Babies0xd3d9ddd0cf0a5f0bfb8f7fceae075df687eaebab1
Pudgy Penguins0xbd3531da5cf5857e7cfaa92426877b022e612cf81

Lastly, to generate the list of recommendations for our set of wallets we will use the ranking of the behavior change in the BEHAVIOR_COLLECTOR category in the last 30 days using Behaviors by Collection endpoint.

Look for the walletsCountChange30d value in the response in each behavior category and rank each collection accordingly.

// Define the JSON response structure
type BehaviorResponse struct {
	AggregatedBehaviors []struct {
		Behavior              string  `json:"behavior"`
		WalletsCount          string  `json:"walletsCount"`
		WalletsPercentage     float64 `json:"walletsPercentage"`
		WalletsCountChange30d string  `json:"walletsCountChange30d"`
	} `json:"aggregatedBehaviors"`
}

type kv struct {
	k string
	v int
}

func ranking(collections []string) {
	client := &http.Client{}

	resultsMap := make(map[string]int)

	for _, col := range collections {
		baseURL := fmt.Sprintf("https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/behaviors/by_collection/%s", col)
		req, _ := http.NewRequest("GET", baseURL, nil)
		req.Header.Add("X-API-Key", "<YOUR API KEY>")
		req.Header.Add("accept", "application/json")

		resp, err := client.Do(req)
		if err != nil {
			log.Fatal(err)
		}

		body, err := io.ReadAll(resp.Body)
		if err != nil {
			log.Fatal(err)
		}

		resp.Body.Close()

		var response BehaviorResponse
		if err := json.Unmarshal(body, &response); err != nil {
			log.Fatal(err)
		}

		for _, behavior := range response.AggregatedBehaviors {
			if behavior.Behavior == "BEHAVIOR_COLLECTOR" {
				walletsCountChange30d, _ := strconv.Atoi(behavior.WalletsCountChange30d)
				resultsMap[col] = walletsCountChange30d
				break
			}
		}
	}

	var pairs []kv
	for key, value := range resultsMap {
		pairs = append(pairs, kv{key, value})
	}

	sort.Slice(pairs, func(i, j int) bool {
		return pairs[i].v > pairs[j].v
	})

	for _, pair := range pairs {
		fmt.Printf("Collection: %s, WalletsCountChange30d: %d\n", pair.k, pair.v)
	}
}
import requests
import json
from operator import itemgetter

# Define the JSON response structure
class BehaviorResponse:
    def __init__(self, data):
        self.aggregated_behaviors = data['aggregatedBehaviors']

collections = ["<collection1>", "<collection2>", "<collection3>"]

results_map = {}

for col in collections:
    base_url = f"https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/behaviors/by_collection/{col}"
    headers = {
        "X-API-Key": "<YOUR API KEY>",
        "accept": "application/json"
    }

    response = requests.get(base_url, headers=headers)

    if response.status_code == 200:
        data = response.json()
        behavior_response = BehaviorResponse(data)

        for behavior in behavior_response.aggregated_behaviors:
            if behavior['behavior'] == "BEHAVIOR_COLLECTOR":
                wallets_count_change_30d = int(behavior['walletsCountChange30d'])
                results_map[col] = wallets_count_change_30d
                break

# sort the results_map by value in descending order
sorted_results = sorted(results_map.items(), key=itemgetter(1), reverse=True)

for pair in sorted_results:
    print(f"Collection: {pair[0]}, WalletsCountChange30d: {pair[1]}")
const axios = require('axios');

let collections = ["<collection1>", "<collection2>", "<collection3>"];
let resultsMap = {};

collections.forEach(async (col) => {
    let url = `https://ethereum-rest.api.mnemonichq.com/audiences/v1beta1/behaviors/by_collection/${col}`;
    let headers = {
        "X-API-Key": "<YOUR API KEY>",
        "accept": "application/json"
    };

    try {
        let response = await axios.get(url, { headers: headers });
        let data = response.data;

        data.aggregatedBehaviors.forEach((behavior) => {
            if (behavior.behavior === "BEHAVIOR_COLLECTOR") {
                let walletsCountChange30d = parseInt(behavior.walletsCountChange30d);
                resultsMap[col] = walletsCountChange30d;
            }
        });

        // sort the resultsMap by value in descending order and print the results
        let sortedResults = Object.entries(resultsMap).sort((a, b) => b[1] - a[1]);

        sortedResults.forEach((pair) => {
            console.log(`Collection: ${pair[0]}, WalletsCountChange30d: ${pair[1]}`);
        });

    } catch (error) {
        console.error(error);
    }
});

Finally, we've got a ranked list of collections that are trending among similar explorers:

CollectionAddressWallets Count Change 30d
Otherdeed Expanded0x790b2cf29ed4f310bf7641f013c65d4560d28371784
DeGods0x8821bee2ba0df28761afff119d66390d594cd280720
Otherside Vessels0x5b1085136a811e55b2bb2ca1ea456ba82126a376697

Method 2

This method is very similar to the above, however, instead of counting the most bought in collections by the segment of wallets, we simply count currently owned collections.

This is a slightly simplified approach with the downside of not counting repurchasing signal as in Method 1, which could be considered as a strong signal in many cases.

To accomplish this, simply use NFTs Owned endpoint to get the list of owned NFTs and collections, and then repeat the same steps as described above.

Summary

The behavior categorization is a powerful tool when combined with audience segmentation that helps build more personal experiences and recommendations. You can tweak the parameters and tailor your solution to the audience or the goals that you are looking to achieve. Try changing the behavior categories and see how it affects the final results.

Join our Discord community to share ideas and ask questions about the behavior categories and the types of recommendations you are building and what kind of results you see!

Happy building!