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:
Category | Description |
---|---|
Collector | A 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. |
Newbie | Wallet 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:
- 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.
- 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 (seewalletsCountChange30d
value in the response). - (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):
Collection | Address |
---|---|
Pudgy Penguins | 0xbd3531da5cf5857e7cfaa92426877b022e612cf8 |
Otherdeed for Otherside | 0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258 |
Doodles | 0x8a90cab2b38dba80c64b7734e58ee1db38b8992e |
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:
Collection | Address | Wallets Count Change 30d |
---|---|---|
ALTS by adidas | 0x749f5ddf5ab4c1f26f74560a78300563c34b417d | 1823 |
Otherdeed Expanded | 0x790b2cf29ed4f310bf7641f013c65d4560d28371 | 784 |
Otherside Vessels | 0x5b1085136a811e55b2bb2ca1ea456ba82126a376 | 697 |
Otherside Koda | 0xe012baf811cf9c05c408e879c399960d1f305903 | 109 |
Cozy Penguins NFT | 0x63d48ed3f50aba950c17e37ca03356ccd6b6a280 | 0 |
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:
Parameter | Value | Description |
---|---|---|
blockTimestampGt | Provide a value that is between 10-30 days ago | Ex: 2023-04-30T14:15:22Z |
labelsAny | LABEL_SALE | Get only sale transfers |
tokenIsSpam | SPAM_FILTER_EXCLUDE | Exclude potential spam |
party | PARTY_RECIPIENT | Get 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:
Collection | Address | Count |
---|---|---|
Milady Maker | 0x5af0d9827e0c53e4799bb226655a1de152a425a5 | 15 |
BEANZ Official | 0x306b1ea3ecdf94ab739f1910bbda052ed4a9f949 | 17 |
Doodles | 0x8a90cab2b38dba80c64b7734e58ee1db38b8992e | 3 |
Otherdeed for Otherside | 0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258 | 2 |
Genesis Adventurers (for Loot) | 0x8db687aceb92c66f013e1d614137238cc698fedb | 2 |
Redacted Remilio Babies | 0xd3d9ddd0cf0a5f0bfb8f7fceae075df687eaebab | 1 |
Pudgy Penguins | 0xbd3531da5cf5857e7cfaa92426877b022e612cf8 | 1 |
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:
Collection | Address | Wallets Count Change 30d |
---|---|---|
Otherdeed Expanded | 0x790b2cf29ed4f310bf7641f013c65d4560d28371 | 784 |
DeGods | 0x8821bee2ba0df28761afff119d66390d594cd280 | 720 |
Otherside Vessels | 0x5b1085136a811e55b2bb2ca1ea456ba82126a376 | 697 |
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!
Updated over 1 year ago