• Lesley

Build an NFT Search with Marketing Cloud

Let’s start dipping our toes into web3, shall we?


You may find yourself in the near future having to access assets that live on the blockchain from the comfort of your web2 home. What is the difference, really? Salesforce Marketing Cloud has its feet firmly planted in web2. We know this because there is a behemoth company that hosts all of your data in “the cloud” which is really just a database that lives in servers that are owned by Salesforce and/or another behemoth company (AWS).


Web3, on the other hand, distributes all the data across a network of servers that have to agree with each other. Two of those companies that we are going to touch on are The Graph and Moralis.


The Graph utilizes Subgraphs, which are a hosted by a network of indexers that index data on the blockchain and store it. The Graph is free to use, but the indexers have to stake GRT tokens in order to prevent bad actors from providing bad indexes. The Graph allows developers to query the indexes via API.


Moralis allows developers to create servers to host their dApps (decentralized Apps) so that they don’t have to buy their own physical servers. In other words, virtual servers. However, what really excites me about Moralis are the fantastic APIs.


This project started with a flaw in OpenSea. OpenSea is the largest marketplace for listing and buying NFTs. There is a huge trend in NFT projects at the moment to generate 10,000 NFTs as a fundraising mechanism. Think of it as funding a start-up, but without the VC pitches. An NFT project can generate, as an example, $4 million in income on the day of the mint, and also have an ongoing stream of revenue when NFTs are resold on a secondary market and the royalties built into the smart contract are automatically captured. OpenSea, and other platforms, allow you to search through these collections for certain traits (hair color, skin color, clothing, facial features, accessories, etc). These traits are defined in the metadata for each NFT. The flaw in OpenSea, and other platforms, is that you cannot search the description field. This is largely because NFT projects generally populate the description field with a description of the project, and it’s the same for every NFT in the collection. However, there has been a recent trend of creating a unique, and fun, description for each individual NFT. The first collection I saw leverage this was Crypto Coven. The second was Meta Angels.






The artwork for Meta Angels is gorgeous, but what really resonates with people is the Angel Type and Description. Examples range from Angel of Sunrises, Angel of Libraries, to Angel of Mansplaining. There are 568 distinct angel types and no two descriptions are alike. Here are four of my Angels:


“You are an angel of coaching. You help children to become their best selves, both on and off the field. You are honored for your ability to motivate and inspire others. You provide guidance and support to those who are growing and learning.”


“You are an angel of sunrises. You bring hope and new beginnings to the world, heralding a fresh start for everyone. You are revered for your ability to help people believe in the power of possibility. You provide optimism and hope to those who seek renewal.”


“You are an angel of snuggles. You wrap your warm wings around those who need it, providing comfort and security. You are revered for your ability to make people feel loved and supported. You provide a sense of safety and belonging to those seeking kindness.”


“You are an angel of prosperity. You bring light and hope to those working toward success, lifting them up in their efforts. You are venerated for your ability to help people work toward financial security. You provide support to those seeking abundance and wealth.”


There were a lot of requests in the community for the ability to search descriptions. I personally really wanted to find an Angel of Early Adopters. Clicking through 10,000 Angels on OpenSea was a futile exercise. So, I started trying to think of ways to build one.


My first attempt began with The Graph. A solution for Crypto Coven with a GitHub repo was brought to my attention. I even tracked down the developer, Nader Dabit, at ETH Denver and he spent about an hour helping me set up the Graph CLI on my computer. Not because it's hard to use, but because the path on my computer was not acting as expected. I also attended his workshop at ETH Denver and diligently followed his instructions in the ETH Denver Workshop repo. However, after meticulously updating the mapping for Meta Angels, I discovered that the key element of using The Graph is that the metadata lives in IPFS. For the purposes of this explanation, let’s just call IPFS a distributed database. IPFS is what NFT projects generally use to reduce the costs of minting artwork directly to the blockchain. Art that is on the blockchain (like Art Blocks) is the most purely decentralized application of NFTs. However, it requires much more computing power, and therefore much more gas, which costs ETH. Most NFT projects are minting a contact that incudes an image URL and that image is stored in IPFS, AWS, or some other file storage platform. NFT projects like Crypto Coven also store their metadata in IPFS. Meta Angles, however, has their metadata stored on their own app. So, I was ultimately not able to create a subgraph using The Graph. This is a solution I plan to use down the road, though, as the ability to query data with APIs is fascinating.


So, to make my life easier, I turned back to a web2 solution. The good news is that I could see the following solutions coming up again in Marketing Cloud life as we dip our toes into web3.


The first step was to get the 10,000 Angels into a Marketing Cloud Data Extension. Fortunately, the metadata for each Angel is JSON formatted.

{
"name": "Meta Angels #1",
"description": "You are an angel of grand visions. You see the world as a place of possibility, and you long to create something new and wonderful. You are venerated for your dedication to fostering connection and opening doors for others. You are a volcano of new ideas.",
"image": "https://metaangelsnft.mypinata.cloud/ipfs/QmWhG8fiKLTcSTkeEcQTa2LNYNEuZM3LpXvFJVtfUHhHzA/1.jpeg",
"dna": "b87a512c11e869eea143169ec6d7a63d78f81a63",
"edition": 1,
"date": 1644298429174,
"attributes": [
{
"trait_type": "Background",
"value": "Blue Sky"
},
{
"trait_type": "Wings",
"value": "Lucky Butterflies"
},
{
"trait_type": "Halo",
"value": "White"
},
{
"trait_type": "Halo Embellishment",
"value": "Santiago Gilded"
},
{
"trait_type": "Skin",
"value": "Light Peach"
},
{
"trait_type": "Eyes",
"value": "Wise Walnut"
},
{
"trait_type": "Mouth",
"value": "Rose"
},
{
"trait_type": "Attire",
"value": "White Tee"
},
{
"trait_type": "Hair",
"value": "Brunette Layered Bob"
},
{
"trait_type": "Angel Archetype",
"value": "Wild Angel"
},
{
"trait_type": "Rarity Rank (#1 Rarest)",
"value": 967,
"max_value": 10000
}
],
"angel_type": "Angel of Grand Visions",
"score": 569.5972403897124,
"external_url": "https://app.metaangelsnft.com/token/1"
}

Each Angel lives at https://app.metaangelsnft.com/metadata, so I was able to run a loop that retrieved each Angel and inserted it into a Data Extension. I used AMPScript’s HTTPGet because it’s so easy, followed by SSJS to process the results. I also used AMPScript to loop because I was starting with AMPScript and it seemed like the easiest way to go. I ran loops of about 750 Angels at a time, as the script timed out if I went much above that. Fortunately, with 10,000 as the total number of Angels, I only had to run the script around 13 or 14 times. It’s important to note that most things in the development world start at 0.


%%[for @i = 0 to 750 do

set @JSON = HTTPGet(concat("https://app.metaangelsnft.com/metadata/",@i,".json")) ]%%

<script runat="server">
Platform.Load("Core", "1")

// it's important to clear out the variables at the start of every loop

var background = '';
var wings = '';
var halo = '';
var embellishment = '';
var skin = '';
var eyes = '';
var mouth = '';
var attire = '';
var hair = '';
var archetype = '';
var rarity = '';

var number = Variable.GetValue("@i");
var str = Variable.GetValue("@JSON");
var obj = Platform.Function.ParseJSON(str);
var name = obj.name;
var description = obj.description;
var image = obj.image;
var angel_type = obj.angel_type;
var external_url = obj.external_url;
var attributes = obj.attributes;

if(attributes.length >= 1) {
for (i = 0; i < attributes.length; i++) {

if (attributes[i].trait_type === "Background") {
var background = attributes[i].value;
} else if (attributes[i].trait_type === "Wings") {
var wings = attributes[i].value;
} else if (attributes[i].trait_type === "Halo") {
var halo = attributes[i].value;
} else if (attributes[i].trait_type === "Halo Embellishment") {
var embellishment = attributes[i].value;
} else if (attributes[i].trait_type === "Skin") {
var skin = attributes[i].value;
} else if (attributes[i].trait_type === "Eyes") {
var eyes = attributes[i].value;
} else if (attributes[i].trait_type === "Mouth") {
var mouth = attributes[i].value;
} else if (attributes[i].trait_type === "Attire") {
var attire = attributes[i].value;
} else if (attributes[i].trait_type === "Hair") {
var hair = attributes[i].value;
} else if (attributes[i].trait_type === "Angel Archetype") {
var archetype = attributes[i].value;
} else if (attributes[i].trait_type === "Rarity Rank (#1 Rarest)") {
var rarity = attributes[i].value;
};
};
};

var rows = Platform.Function.UpsertData("Meta Angels",
["Number"],[number],
["name","description","image","angel_type","external_url","Background","Wings","Halo","Halo_Embellishment",
"Skin","Eyes","Mouth","Attire","Hair","Angel_Archetype","Rarity_Rank"],
[name,description,image,angel_type,external_url,background,wings,halo,embellishment,skin,eyes,mouth,attire,hair,archetype,rarity]);
</script>

%%[ next ]%%

I created a Data Extension called "Meta Angels" with the field "Number" as a primary key so that I could re-run the script if it timed out without just appending more rows.


I also could have simplified the field name and value pairs if the name of the Rarity Rank didn’t include special characters. If you are lucky enough to experience metadata with simple “trait_type” names, then you can skip the array loop that I built and just go straight to building the UpsertData function. I also recommend making sure all the fields in the destination data extension are only one word. You will thank me when we get to the next step.


This script successfully populated a data extension with 10,000 Angels.


Now onto the front end.


I built a very basic front end because I was only trying to achieve the goal of searching descriptions. This page is very easy to add to and more parameters could easily be added and returned. The first thing I did was add Bootstrap to a Cloud Page in Marketing Cloud. This allowed me to create a very basic and nice looking form:


<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<div class="mb-3">
<label for="angelSearch" class="form-label">Angel Search Term</label>
<input type="text" class="form-control" id="angelSearch" name="angelSearch" aria-describedby="angelSearch">
<div id="angelSearchHelp" class="form-text">Keep it simple, but not too simple.</div>
</div>
<input name="submitted" type="hidden" value="true">

<button type="submit" class="btn btn-danger">Submit</button>

</form>
</div>
</div>
</div>

I chose to just have this page reload on itself, rather than an Ajax call, because it was the easiest way to handle this. Upon reload, I use SSJS to format a query to the Meta Angels data extension. This is a very powerful SSJS tool and it runs much faster than a query in Query Studio.


%%[SET @searchTerm = RequestParameter("angelSearch")]%%

<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">

Search Term: %%=v(@searchTerm)=%%<br><br>

<script runat="server">
Platform.Load("core", "1.1.1");

var angelsDE = DataExtension.Init("DE-EXTERNAL-KEY");

var input = Variable.GetValue("@searchTerm");

var filter = {Property:"description",SimpleOperator:"Like",Value:input};
var data = angelsDE.Rows.Retrieve(filter);
Write("Number of Results: " + data.length + "<br><br>");


for (var i = 0; i < data.length; i++) {
Write("Angel Number: " + data[i].Number + '<br/>');
Write("Angel Type: " + data[i].angel_type + '<br/>');
Write("Angel Archetype: " + data[i].Angel_Archetype + '<br/>');
Write("Rarity Rank: " + data[i].Rarity_Rank + '<br/>');
Write("Description: " + data[i].description + '<br/><br/>');
Write('<img src="' + data[i].image + '" width="250" height="auto">');
Write("<br><br><hr>");
};

</script>
</div>
</div>
</div>

I applied very basic styling to the page. Feel free to input search terms HERE to see the results.


Here is the full code for the front end page:


%%[IF RequestParameter("submitted") == true THEN

SET @searchTerm = RequestParameter("angelSearch")

]%%

<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
Search Term: %%=v(@searchTerm)=%%<br><br>

<script runat="server">
Platform.Load("core", "1.1.1");

var angelsDE = DataExtension.Init("DE-EXTERNAL-KEY");

var input = Variable.GetValue("@searchTerm");

var filter = {Property:"description",SimpleOperator:"Like",Value:input};
var data = angelsDE.Rows.Retrieve(filter);
Write("Number of Results: " + data.length + "<br><br>");


for (var i = 0; i < data.length; i++) {
Write("Angel Number: " + data[i].Number + '<br/>');
Write("Angel Type: " + data[i].angel_type + '<br/>');
Write("Angel Archetype: " + data[i].Angel_Archetype + '<br/>');
Write("Rarity Rank: " + data[i].Rarity_Rank + '<br/>');
Write("Description: " + data[i].description + '<br/><br/>');
Write('<img src="' + data[i].image + '" width="250" height="auto">');
Write("<br><br><hr>");
};

</script>
</div>
</div>
</div>

%%[ELSE]%%

<div class="container">
<div class="row">
<div class="col-md-6 offset-md-3">
<form action="%%=RequestParameter('PAGEURL')=%%" method="post">
<div class="mb-3">
<label for="angelSearch" class="form-label">Angel Search Term</label>
<input type="text" class="form-control" id="angelSearch" name="angelSearch" aria-describedby="angelSearch">
<div id="angelSearchHelp" class="form-text">Keep it simple, but not too simple.</div>
</div>
<input name="submitted" type="hidden" value="true">

<button type="submit" class="btn btn-danger">Submit</button>

</form>
</div>
</div>
</div>

%%[ENDIF]%%

There is a lot more data that could be added to this page, such as a link to OpenSea. I also did not touch on my use of the Moralis APIs here, and will do that in my next post. Stay tuned....

100 views0 comments