Helius

1/17/2023 6:12:44 PM

Building an NFT Sales Bot with JavaScript and Solana

Frame 10.png

TL;DR — If you’re more of a “talk is cheap, show me the code” type, you can find the 

Background

If you’re looking to learn how to make an NFT sales bot on Solana right now, your options aren’t great. There are a few open-source bot repos and they’re well written — but I think they’re more complicated than necessary.

Also, for some weird reason, new devs in Solana automatically assume that you must be a Rust wizard to build anything. This is not true. You do not need to know Rust to build on Solana (unless you need to write contracts).

And worse, if you’re not a developer, your only choices are to:

a) Pay a random developer (and get overcharged)

b) Buy an existing sales-bot solution (and get overcharged)

This is pretty annoying — so I decided I’d make a bunch of free bots to help out.

1*8pBKsTYQBapUVycI9KtJ-w.webp

Obviously, this wasn’t scalable. But after making 9 or so bots, I like to think I’ve got a good enough grasp on this topic to be able to write a small guide on it.

High-Level Overview (optional)

What we’re building: a sales bot that tracks a given NFT’s sales across all major Solana marketplaces and posts the sale details to a Discord channel.

Before diving into the code, it’s helpful to first establish a conceptual understanding.

The way we’re going to accomplish this will be as follows:

  1. Figure out the NFT collection’s account/address for receiving royalties
  2. Continuously poll this address for new transaction signatures
  3. Loop through the transaction signatures, fetch their transaction details, and check for valid NFT sales
  4. If valid, fetch the NFT’s metadata and post the details to Discord
  5. Go to Step 2 — making sure to only poll for transactions that occur after the latest observed transaction

Polling vs. Streaming (optional)

Whenever you’re dealing with “live-feed” data — you basically have two options: polling or streaming/PubSub.

Generally speaking, using a streaming/PubSub pattern, where you simply subscribe to your NFT collection’s main account and listen for changes, would be the better approach.

For this tutorial, we’re going to poll for the data. This is because 1) it’s easier to code and explain conceptually (IMO), and 2) this makes it a lot easier to backfill historic sale data.

Setup

First, make sure you have Node JS installed (version 14.17.0 or higher), I recommend using NVM. Then, open up a terminal, create a new folder,cd into it, and initialize npm.

$ mkdir nft-sales-bot
$ cd nft-sales-bot
$ npm init

Once repo is initialzed, we really only need 3 things:

$ npm i @solana/web3.js @metaplex/js axios

Solana/web3.js is a library for interacting with Solana nodes on-chain (fetching transactions, account info, signatures, etc.,).

Metaplex is a set of tools/contracts/standards to interact with Solana NFTs.

And axios is a library for making HTTP requests.

The Code

(Before proceeding, I want to emphasize that the code here is optimized exclusively for ease of understanding, it’s by no means a good pattern to use in general)

First, import the libraries we just installed.

1*-wjHKFRX4I_B7Pfru-IVXw.webp

There are a few things going on here. First, we’re validating that the project address and Discord URL are being passed in to the bot (more on this later). Second, we’re connecting to the mainnet for Metaplex and Solana. Alternatively, you can connect to devnet for testing. Third, we’re destructing the Metadata program from Metaplex, this’ll help us fetch NFT metadata. Fourth, we’re creating a new PublicKey object from the PROJECT_ADDRESS environment variable—this’ll be necessary for passing into the solana/web3 library. Finally, we’re setting a polling interval for how often we want our bot to check for new sales. I recommend setting this to be somewhere between 2000–5000ms at least. This is because the default Solana RPC nodes have rate-limits and your requests won’t go through if you’re making too many calls in a short time period. You can use an RPC provider like GenesysGo to make unlimited calls, though this won’t be free.

Now, let’s add the major Solana NFT marketplaces and their program addresses. Whenever an NFT sale occurs on a marketplace, the marketplace’s program address obviously has to be involved and you can observe this on-chain (here is an example for Magic Eden).

1*5RtRCUekbcrmJPzv87SUTA.webp

Let’s now start the main function for running the sales bot.

1*x8AnfGBq1L5Ujl4HZ4jDBw.webp

You can ignore the unassigned variables for now — the main thing to note here is the getSignaturesForAddress function. This returns confirmed signatures for transactions involving the given address, backwards in time from the most recent confirmed block (or whatever signature you pass in the options). Basically, if the main project address gets any new transactions, this function will get them. If there are no transaction, we wait the polling interval duration, and check again continuously in an infinite loop. You can find the implementation of the timer fn and any other helper fns in the source code.

Let’s finish the main function.

1*FHlKNuHQnbQtezxzVpvK2w.webp

Remember that getSignaturesForAddress returns transactions in order of descending time. This means that we need to start looping from the last element of the signatures array to display the sales chronologically. For each signature, we can call the getTransaction method on the web3 library to fetch the exact transaction details. With the details on hand, we first check if the transaction has any errors, in which case we know there wouldn’t have been a successful sale.

Next, we convert the blockTime which is in Unix time to a more readable date object. Then we do a simple calculation to get the price of the sale and also see which marketplace the last account maps to (in my experience, the last account is always the marketplace identifier — I’m not sure if this is universally true since I’m stupid, if you want to be very safe, you can look through all of the accounts).

We then get the metadata of the NFT that was exchanged — and with that we have all the information necessary to post the sale details on Discord.

Before polling again, we also want to make sure that we’re only fetching the transactions that have occurred since the last time that we polled. In order to do this, the getSignaturesForAddress method takes in an until option which we can set to be the most recent transaction that we know of, lastKnownSignature.

(Note: getSignaturesForAddress by default returns the last 1000 signatures. This means that the sales bot will actually start by replaying old sales. I like doing this in order to backfill historic data on Discord — which the community enjoys watching. If you want to only show new sales, you need to modify this. For example, you can set a bootupDate when your bot first starts up and subsequently check that all transactions’ dates must be greater than this before posting them)

Getting Metadata

It’s now time to define the getMetadata function that we referenced earlier.

1*db7mjpntWW1_dupiDXDMzw.webp

First we get the program derived address (PDA) for our token’s address and then we plug it into Metaplex’s built in Metadata.load to fetch a link to the metadata. We then use this link to get the actual metadata.

I’m not going to lie, this is quite slow in practice. We’re making successive network calls and the last call is usually made to the IPFS, which is a huge bottleneck.

Luckily, we can actually optimize this somewhat by leveraging the help of our trusted friends, Magic Eden — Solana’s biggest and IMO best, NFT marketplace.

1*poECZjkbNcl545DFIR7gRA.webp

Since these marketplaces have to deal with tons of metadata per day, they very likely cache this data and thus are vastly more performant. And fortunately for us, they also expose an unofficial public endpoint. Since this is an unofficial endpoint, which is subject to change at any moment, I don’t really encourage you to use this method unless performance is actually an issue for you.

Posting to Discord

In order to post to Discord, we first need to obtain a webhook URL. To do this: right click on your server -> server settings -> integrations -> webhooks -> new webhook. Here you can name the webhook and configure which channel you want it to send the sales to. Once configured — just click “Copy Webhook URL”.

1*jimeeTb_uPDkEchKsG7Emg.webp

Next, let’s write the function for sending the sales.

1*CQZ8etUEJ062Gm_ZJa0s6Q.webp

If you want to learn more about how to customize this, take a look at this doc.

Running the Bot

The code is now finished. In order to run it, we need a few more things.

First, we need to figure out what the process.env.PROJECT_ADDRESS should be. To do this, I usually just pull up the NFT on Solana Explorer and identify which creators are getting the majority of the royalties.

1*nsXJfZr3pV6gvZ-q9wjQkg.webp

Second, with this and the Discord URL in hand, simply add a line for running the bot runSalesBot(); and finally run:

$ PROJECT_ADDRESS=8u...s DISCORD_URL=paste-url-here node sales_bot.js

Hint: for hosting the bot, I like to use Replit on always-on mode, but you can use Heroku or even your own machine.

Conclusion / Next Steps

You can find the finished code here.

I want to reiterate that I’m not claiming this is the best way to build a bot (or even a good way). I’m sure there are countless other ways to make this a lot better. My primary aim for this tutorial was to write something that you can easily read/understand and reason about conceptually. The whole thing is just around 100 lines. In practice, you’d ideally use a PubSub model and a better project structure with well tested helper functions.

If you liked this tutorial or have any questions/tips/corrections/suggestions, give me a follow on Twitter (@turkmmtz).