lafargue.cc Blog of a French web developper and blockchain enthusiast

Building a $1M NFT Trading Business

$1M transaction volume in 6 months. This is the story of my brief experience trading NFTs.

You can see all the trades I made in the table at the bottom of this post

I discovered Bitcoin in 2013. In 2021, I launched my first blockchain business.

Early 2021, I got interested in the Decentraland project. Its goal is to build a virtual world on the Ethereum blockchain. I believe the future of the metaverse is on the blockchain because in a virtual world, ownership is key.

Decentraland screenshot

In Decentraland, owning LAND grants you the rights to deploy whatever 3D creation you want inside the virtual world. The way I see this project is like a showroom for the blockchain. In its current state, blockchain is not accessible for the general public. In this early adopter stage, it is important to include users excited about the technology, regardless of technical background. Decentraland is the place for building blockchain experiences that are easier to access.


First steps

On January 19th, 2021, I deposited 1000€ on Binance. I purchased 8065 MANA (Decentraland’s own currency) which I withdrew to my Ethereum wallet. I used 8000 MANA to purchase 1 LAND on the Decentraland marketplace. Basically, I bought a piece of virtual LAND for about 1000€.

Here’s what that LAND looks like on the map:

First LAND image

About two months later, on March 17th, I sold that LAND for 7214 MANA. Atari had annouced an upcoming project in Decentraland which shot the value of MANA up almost 10x. Although I had lost MANA relative to my purchase price of the LAND, my stake was now worth $7,677.

LAND Date Event Amount $USD Value
Image -74, -48 2021-01-21 Buy - 8,000 MANA -$979.47
Image -74, -48 2021-03-17 Sell + 7,214 MANA +$7,677.59

Riding the high of this lucky investment, I began to be more interested in the LAND market. I continued to buy and sell a few LAND tokens to see if I could turn a profit.

LAND Date Event Amount $USD Value
Image 81, -18 2021-03-17 Buy - 6,899 MANA -$7,342.32
Image 81, -18 2021-03-18 Sell + 7,505 MANA +$7,370.13
Image -75, -105 2021-03-18 Buy - 6,422 MANA -$6,306.94
Image -75, -105 2021-04-13 Sell + 4,388 MANA +$4,910.38
Image -104, -88 2021-04-17 Buy - 2 WETH -$4,675.13
Image -104, -88 2021-04-18 Sell + 3,998 MANA +$5,395.87
Image -150, -136 2021-07-22 Buy - 1.17 WETH -$2,368.21
Image -150, -136 2021-07-23 Sell + 4,475 MANA +$3,023.31
Image -23, -99 2021-08-23 Buy - 0.99 WETH -$3,300.83
Image -23, -99 2021-08-24 Sell + 6,386 MANA +$5,440.33


Automating

At this point I came to the understanding that all the best LAND deals were gone within minutes if not seconds. If I wanted to be first, I needed to automate. So I set up a Telegram bot that would send me notifications when LAND was listed at low prices. Unfortunately I don’t have any screenshots of that bot but basically it would send me messages like this on Telegram in real time:

⚠️ LAND BARGAIN ALERT ⚠️ 32,88 has been listed for 500 MANA

These alerts helped me pick up some LAND on the cheap.

LAND Date Event Amount $USD Value
Image 32, 88 2021-09-06 Buy - 500 MANA -$520.99
Image -19, 23 2021-09-06 Buy - 500 MANA -$520.99
Image 32, 88 2021-09-06 Sell + 4,872 MANA +$5,076.65

I also built a custom trading bot for the Opensea market. This market was interesting because it was possible to place offers on LAND at no cost. Unfortunately, there was no way to make a blanket offer on all available LAND, offers needed to be created individually. What made things worst is the fluctuating value of the LAND requiring the value of offers to be re-evalutated on a daily basis. Placing offers manually, every day, on the thousands of existing LAND was not feasible. For this reason I built bot that could do it for me. This strategy proved very fruitful.

LAND Date Event Amount $USD Value
Image -142, -119 2021-09-09 Buy - 0.92 WETH -$3,164.47
Image -142, -119 2021-09-10 Sell + 4,658 MANA +$3,811.68
Image -59, -108 2021-09-13 Buy - 0.92 WETH -$3,028.92
Image -19, 23 2021-09-13 Sell + 9,340 MANA +$7,669.93
Image -59, -108 2021-09-15 Sell + 5,948 MANA +$5,124.41
Image -70, -33 2021-09-22 Buy - 0.92 WETH -$2,817.32
Image -39, 110 2021-09-22 Buy - 0.92 WETH -$2,817.32
Image -70, -33 2021-09-23 Sell + 1.17 ETH +$3,690.9
Image -40, 98 2021-09-24 Buy - 0.92 WETH -$2,698.9
Image -40, 98 2021-09-27 Sell + 1.32 ETH +$3,853.63
Image -39, 110 2021-09-27 Sell + 1.36 ETH +$3,967.8
Image -23, -138 2021-10-08 Buy - 0.92 WETH -$3,282.77
Image -23, -138 2021-10-08 Sell + 1.22 ETH +$4,342.21
Image -110, -150 2021-10-10 Buy - 0.92 WETH -$3,158.69
Image -110, -150 2021-10-14 Sell + 1.09 ETH +$4,140.03
Image 20, 12 2021-10-14 Buy - 0.92 WETH -$3,485.78
Image -17, -119 2021-10-15 Buy - 0.92 WETH -$3,559.5
Image 9, -48 2021-10-15 Buy - 0.92 WETH -$3,559.5
Image 9, -48 2021-10-17 Sell + 1.09 ETH +$4,200.55
Image 20, 12 2021-10-19 Sell + 1.28 ETH +$4,952.47
Image -17, -119 2021-10-19 Sell + 1.02 ETH +$3,969.54
Image -77, -118 2021-10-23 Buy - 0.85 WETH -$3,549.58
Image -77, -118 2021-10-26 Sell + 0.95 ETH +$3,905.9
Image -124, -55 2021-10-31 Buy - 1.12 WETH -$4,806.81
Image -124, -55 2021-10-31 Sell + 2.6 ETH +$11,168.21
Image 54, -134 2021-11-01 Buy - 3,000 MANA -$9,144.39
Image 54, -134 2021-11-02 Sell + 2.29 ETH +$10,524.05
Image 11, -117 2021-11-04 Buy - 1.82 WETH -$8,289
Image 68, -17 2021-11-04 Buy - 1.82 WETH -$8,289
Image 11, -117 2021-11-04 Sell + 1.94 ETH +$8,802.89
Image 68, -17 2021-11-04 Sell + 2.12 ETH +$9,599.14
Image -54, 146 2021-11-05 Buy - 1.69 WETH -$7,592.26
Image -54, 146 2021-11-05 Sell + 1.82 ETH +$8,168.33
Image 69, -80 2021-11-06 Buy - 1.69 WETH -$7,647.93
Image 55, -119 2021-11-06 Buy - 1.69 WETH -$7,647.93
Image 69, -80 2021-11-06 Sell + 2.51 ETH +$11,329.52
Image 61, -121 2021-11-06 Buy - 1.69 WETH -$7,647.93
Image 55, -119 2021-11-07 Sell + 2.41 ETH +$11,119.2
Image 61, -121 2021-11-08 Sell + 2.31 ETH +$11,116.95
Image -30, 4 2021-11-09 Buy - 1.69 WETH -$8,107.69
Image 28, -143 2021-11-09 Buy - 1.69 WETH -$8,107.69
Image 28, -143 2021-11-10 Sell + 2.12 ETH +$9,802.23
Image -30, 4 2021-11-10 Sell + 3.19 ETH +$14,771.09
Image 57, -7 2021-11-10 Buy - 1.69 WETH -$7,827.76
Image -127, -74 2021-11-11 Buy - 1.69 WETH -$8,023.83
Image 57, -7 2021-11-12 Sell + 2.0 ETH +$9,331.11
Image 129, -19 2021-11-12 Buy - 1.69 WETH -$7,890.93
Image 129, -19 2021-11-12 Sell + 1.86 ETH +$8,694.29
Image -127, -74 2021-11-12 Sell + 2.86 ETH +$13,337.31
Image -59, -127 2021-11-13 Buy - 3,250 MANA -$10,536.03
Image -59, -128 2021-11-13 Buy - 3,280 MANA -$10,633.28
Image 42, 74 2021-11-18 Buy - 1.72 WETH -$6,850.8
Image 42, 74 2021-11-18 Sell + 3.09 ETH +$12,357.345
Image 140, -30 2021-11-18 Buy - 1.72 WETH -$6,850.8
Image 140, -30 2021-11-19 Sell + 3.11 ETH +$13,371.8
Image -55, -6 2021-11-20 Buy - 1.72 WETH -$7,618.03
Image 26, -145 2021-11-20 Buy - 1.72 WETH -$7,618.03
Image 26, -145 2021-11-20 Sell + 3.31 ETH +$14,595.06
Image -59, -127 2021-11-21 Sell + 2.86 ETH +$12,182.93
Image -59, -128 2021-11-21 Sell + 2.86 ETH +$12,182.93
Image -55, -6 2021-11-22 Sell + 3.33 ETH +$13,639.64
Image -65, 142 2021-11-24 Buy - 1.72 WETH -$7,322.92
Image -65, 142 2021-11-25 Sell + 3.72 ETH +$16,848.9
Image -30, -100 2021-11-25 Buy - 3,000 MANA -$15,603.98
Image -131, -145 2021-11-25 Buy - 2.72 WETH -$12,278.81
Image -131, -145 2021-11-25 Sell + 0 ETH +$0
Image 82, -17 2021-11-26 Buy - 3.12 WETH -$12,637.89
Image 82, -17 2021-11-28 Sell + 4.12 ETH +$17,725.7
Image 141, 49 2021-11-28 Buy - 2.52 WETH -$10,829.57
Image -36, 149 2021-11-28 Buy - 2.52 WETH -$10,829.57
Image 25, 150 2021-11-28 Buy - 2.52 WETH -$10,829.57
Image 141, 49 2021-11-28 Sell + 3.32 ETH +$14,247.61
Image -36, 149 2021-11-28 Sell + 3.32 ETH +$14,247.61
Image 25, 150 2021-11-28 Sell + 3.41 ETH +$14,666.65
Image -30, -100 2021-11-29 Sell + 4.7 ETH +$20,905.68
Image -140, -53 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image -140, -52 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image -141, -51 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image 50, 132 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image -108, -66 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image -107, -67 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image -141, -51 2021-12-04 Sell + 3.27 ETH +$13,470.86
Image -107, -67 2021-12-04 Sell + 3.75 ETH +$15,481.44
Image -140, -52 2021-12-05 Sell + 3.46 ETH +$14,541.02
Image -140, -53 2021-12-07 Sell + 3.46 ETH +$14,916.95
Image -108, -66 2021-12-10 Sell + 3.36 ETH +$13,127
Image 50, 132 2021-12-10 Sell + 4.19 ETH +$16,361.19
Image -140, -54 2021-12-13 Buy - 2.81 WETH -$10,640.78
Image -59, 105 2021-12-15 Buy - 2.81 WETH -$11,267.32
Image -140, -54 2021-12-17 Sell + 3.75 ETH +$14,550.92
Image 47, 143 2022-01-03 Buy - 3.21 WETH -$12,089.61
Image -59, 105 2022-01-03 Sell + 4.39 ETH +$16,518.66
Image 47, 143 2022-01-08 Sell + 4.53 ETH +$13,967.63
Image -28, -49 2022-01-12 Buy - 3.71 WETH -$12,513.92
Image -28, -49 2022-01-29 Sell + 7.61 ETH +$17,032.11
Image 83, -36 2022-02-02 Buy - 4.27 WETH -$11,499.81
Image 83, -36 2022-02-05 Sell + 5.22 ETH +$15,728.35
Image 86, -18 2022-02-11 Buy - 4.37 WETH -$12,637.95
Image 68, -139 2022-02-16 Buy - 4.37 WETH -$13,746.92


Mother Lode

The last piece of this business came when Opensea made changes to their platform that made my bot obsolete. I spotted a unique opportunity that piqued my interest. Inside Decentraland, there are casinos where people play for cryptocurrency which is worth real money. Some of these casinos have processed millions of dollars worth of bets.

In the Decentraland Discord server, I came into contact with a seller who wanted to sell a land lease for the “Flamingos Casino” that had yet to open. This “lease” would grant the holder a profit share of the revenues generated in the casino on that LAND. I checked the historical prices of these leases and one had recently sold for 22 ETH. I offered the seller 9 ETH. Initially he refused but months later he came back and sold it to me.

I immediately listed it on the market for 21 ETH and advertised the sale daily in the Discord server. Within 10 days, I pocketed 20.48 ETH after fees for more than 11 ETH profit

LAND Date Event Amount $USD Value
Image -129, 118 2022-03-03 Buy - 9 ETH -$25,503.39
Image -129, 118 2022-03-13 Sell + 20.48 ETH +$51,532.09

Overall, within about a year, my initial investment of 1000€ had grown to be worth over $100,000 and my total transaction volume over a 6 months period was more than $1M.


Total Transaction Volume

ETH WETH MANA $USD Value
RECEIVED 149.13 ETH 0 WETH 58,782 MANA $624,488
SPENT 9 ETH 102.47 WETH 34,851 MANA $488,920
TOTAL +140.13 ETH -102.47 WETH +23,931 MANA +$135,568

TOTAL TRANSACTION VOLUME: $1.11M


Full Transaction List

LAND Date Event Amount $USD Value
Image -74, -48 2021-01-21 Buy - 8,000 MANA -$979.47
Image -74, -48 2021-03-17 Sell + 7,214 MANA +$7,677.59
Image 81, -18 2021-03-17 Buy - 6,899 MANA -$7,342.32
Image 81, -18 2021-03-18 Sell + 7,505 MANA +$7,370.13
Image -75, -105 2021-03-18 Buy - 6,422 MANA -$6,306.94
Image -75, -105 2021-04-13 Sell + 4,388 MANA +$4,910.38
Image -104, -88 2021-04-17 Buy - 2 WETH -$4,675.13
Image -104, -88 2021-04-18 Sell + 3,998 MANA +$5,395.87
Image -150, -136 2021-07-22 Buy - 1.17 WETH -$2,368.21
Image -150, -136 2021-07-23 Sell + 4,475 MANA +$3,023.31
Image -23, -99 2021-08-23 Buy - 0.99 WETH -$3,300.83
Image -23, -99 2021-08-24 Sell + 6,386 MANA +$5,440.33
Image 32, 88 2021-09-06 Buy - 500 MANA -$520.99
Image -19, 23 2021-09-06 Buy - 500 MANA -$520.99
Image 32, 88 2021-09-06 Sell + 4,872 MANA +$5,076.65
Image -142, -119 2021-09-09 Buy - 0.92 WETH -$3,164.47
Image -142, -119 2021-09-10 Sell + 4,658 MANA +$3,811.68
Image -59, -108 2021-09-13 Buy - 0.92 WETH -$3,028.92
Image -19, 23 2021-09-13 Sell + 9,340 MANA +$7,669.93
Image -59, -108 2021-09-15 Sell + 5,948 MANA +$5,124.41
Image -70, -33 2021-09-22 Buy - 0.92 WETH -$2,817.32
Image -39, 110 2021-09-22 Buy - 0.92 WETH -$2,817.32
Image -70, -33 2021-09-23 Sell + 1.17 ETH +$3,690.9
Image -40, 98 2021-09-24 Buy - 0.92 WETH -$2,698.9
Image -40, 98 2021-09-27 Sell + 1.32 ETH +$3,853.63
Image -39, 110 2021-09-27 Sell + 1.36 ETH +$3,967.8
Image -23, -138 2021-10-08 Buy - 0.92 WETH -$3,282.77
Image -23, -138 2021-10-08 Sell + 1.22 ETH +$4,342.21
Image -110, -150 2021-10-10 Buy - 0.92 WETH -$3,158.69
Image -110, -150 2021-10-14 Sell + 1.09 ETH +$4,140.03
Image 20, 12 2021-10-14 Buy - 0.92 WETH -$3,485.78
Image -17, -119 2021-10-15 Buy - 0.92 WETH -$3,559.5
Image 9, -48 2021-10-15 Buy - 0.92 WETH -$3,559.5
Image 9, -48 2021-10-17 Sell + 1.09 ETH +$4,200.55
Image 20, 12 2021-10-19 Sell + 1.28 ETH +$4,952.47
Image -17, -119 2021-10-19 Sell + 1.02 ETH +$3,969.54
Image -77, -118 2021-10-23 Buy - 0.85 WETH -$3,549.58
Image -77, -118 2021-10-26 Sell + 0.95 ETH +$3,905.9
Image -124, -55 2021-10-31 Buy - 1.12 WETH -$4,806.81
Image -124, -55 2021-10-31 Sell + 2.6 ETH +$11,168.21
Image 54, -134 2021-11-01 Buy - 3,000 MANA -$9,144.39
Image 54, -134 2021-11-02 Sell + 2.29 ETH +$10,524.05
Image 11, -117 2021-11-04 Buy - 1.82 WETH -$8,289
Image 68, -17 2021-11-04 Buy - 1.82 WETH -$8,289
Image 11, -117 2021-11-04 Sell + 1.94 ETH +$8,802.89
Image 68, -17 2021-11-04 Sell + 2.12 ETH +$9,599.14
Image -54, 146 2021-11-05 Buy - 1.69 WETH -$7,592.26
Image -54, 146 2021-11-05 Sell + 1.82 ETH +$8,168.33
Image 69, -80 2021-11-06 Buy - 1.69 WETH -$7,647.93
Image 55, -119 2021-11-06 Buy - 1.69 WETH -$7,647.93
Image 69, -80 2021-11-06 Sell + 2.51 ETH +$11,329.52
Image 61, -121 2021-11-06 Buy - 1.69 WETH -$7,647.93
Image 55, -119 2021-11-07 Sell + 2.41 ETH +$11,119.2
Image 61, -121 2021-11-08 Sell + 2.31 ETH +$11,116.95
Image -30, 4 2021-11-09 Buy - 1.69 WETH -$8,107.69
Image 28, -143 2021-11-09 Buy - 1.69 WETH -$8,107.69
Image 28, -143 2021-11-10 Sell + 2.12 ETH +$9,802.23
Image -30, 4 2021-11-10 Sell + 3.19 ETH +$14,771.09
Image 57, -7 2021-11-10 Buy - 1.69 WETH -$7,827.76
Image -127, -74 2021-11-11 Buy - 1.69 WETH -$8,023.83
Image 57, -7 2021-11-12 Sell + 2.0 ETH +$9,331.11
Image 129, -19 2021-11-12 Buy - 1.69 WETH -$7,890.93
Image 129, -19 2021-11-12 Sell + 1.86 ETH +$8,694.29
Image -127, -74 2021-11-12 Sell + 2.86 ETH +$13,337.31
Image -59, -127 2021-11-13 Buy - 3,250 MANA -$10,536.03
Image -59, -128 2021-11-13 Buy - 3,280 MANA -$10,633.28
Image 42, 74 2021-11-18 Buy - 1.72 WETH -$6,850.8
Image 42, 74 2021-11-18 Sell + 3.09 ETH +$12,357.345
Image 140, -30 2021-11-18 Buy - 1.72 WETH -$6,850.8
Image 140, -30 2021-11-19 Sell + 3.11 ETH +$13,371.8
Image -55, -6 2021-11-20 Buy - 1.72 WETH -$7,618.03
Image 26, -145 2021-11-20 Buy - 1.72 WETH -$7,618.03
Image 26, -145 2021-11-20 Sell + 3.31 ETH +$14,595.06
Image -59, -127 2021-11-21 Sell + 2.86 ETH +$12,182.93
Image -59, -128 2021-11-21 Sell + 2.86 ETH +$12,182.93
Image -55, -6 2021-11-22 Sell + 3.33 ETH +$13,639.64
Image -65, 142 2021-11-24 Buy - 1.72 WETH -$7,322.92
Image -65, 142 2021-11-25 Sell + 3.72 ETH +$16,848.9
Image -30, -100 2021-11-25 Buy - 3,000 MANA -$15,603.98
Image -131, -145 2021-11-25 Buy - 2.72 WETH -$12,278.81
Image -131, -145 2021-11-25 Sell + 0 ETH +$0
Image 82, -17 2021-11-26 Buy - 3.12 WETH -$12,637.89
Image 82, -17 2021-11-28 Sell + 4.12 ETH +$17,725.7
Image 141, 49 2021-11-28 Buy - 2.52 WETH -$10,829.57
Image -36, 149 2021-11-28 Buy - 2.52 WETH -$10,829.57
Image 25, 150 2021-11-28 Buy - 2.52 WETH -$10,829.57
Image 141, 49 2021-11-28 Sell + 3.32 ETH +$14,247.61
Image -36, 149 2021-11-28 Sell + 3.32 ETH +$14,247.61
Image 25, 150 2021-11-28 Sell + 3.41 ETH +$14,666.65
Image -30, -100 2021-11-29 Sell + 4.7 ETH +$20,905.68
Image -140, -53 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image -140, -52 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image -141, -51 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image 50, 132 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image -108, -66 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image -107, -67 2021-12-04 Buy - 2.87 WETH -$11,756.9
Image -141, -51 2021-12-04 Sell + 3.27 ETH +$13,470.86
Image -107, -67 2021-12-04 Sell + 3.75 ETH +$15,481.44
Image -140, -52 2021-12-05 Sell + 3.46 ETH +$14,541.02
Image -140, -53 2021-12-07 Sell + 3.46 ETH +$14,916.95
Image -108, -66 2021-12-10 Sell + 3.36 ETH +$13,127
Image 50, 132 2021-12-10 Sell + 4.19 ETH +$16,361.19
Image -140, -54 2021-12-13 Buy - 2.81 WETH -$10,640.78
Image -59, 105 2021-12-15 Buy - 2.81 WETH -$11,267.32
Image -140, -54 2021-12-17 Sell + 3.75 ETH +$14,550.92
Image 47, 143 2022-01-03 Buy - 3.21 WETH -$12,089.61
Image -59, 105 2022-01-03 Sell + 4.39 ETH +$16,518.66
Image 47, 143 2022-01-08 Sell + 4.53 ETH +$13,967.63
Image -28, -49 2022-01-12 Buy - 3.71 WETH -$12,513.92
Image -28, -49 2022-01-29 Sell + 7.61 ETH +$17,032.11
Image 83, -36 2022-02-02 Buy - 4.27 WETH -$11,499.81
Image 83, -36 2022-02-05 Sell + 5.22 ETH +$15,728.35
Image 86, -18 2022-02-11 Buy - 4.37 WETH -$12,637.95
Image 68, -139 2022-02-16 Buy - 4.37 WETH -$13,746.92
Image -129, 118 2022-03-03 Buy - 9 ETH -$25,503.39
Image -129, 118 2022-03-13 Sell + 20.48 ETH +$51,532.09

Developping a profitable NFT trading bot

This blog post outlines how I developped a profitable NFT trading bot in early 2023 after attending Le Wagon web development bootcamp in Nantes, France. I already had prior experience coding but after the bootcamp, I wanted to put into practice the new things I’d learned, notably the Ruby programming language and MVC architecture. To develop this project, I used the Sinatra micro-framework coupled with a PostgreSQL database and a dockerized Node.js instance for the transaction signing logic.

Bot screenshot

The plan

First a quick recap of my prior experience with NFT trading bots:

  • In 2021, I developped an OpenSea mass offer bot which made over $50k in a 6 months period.
  • The bot exploited an asymetry between buy and sell side orders.
  • Sellers might be short on time and wanting to sell their NFT right now, without waiting for a buyer to show up.
  • My bot sent offers to buy each individual NFT in a collection at a discount compared to the cheapest NFT listed for that collection.

For example, if the cheapest NFT in the Bing Bong collection was listed for 1 ETH, I would send an offer to each holder of a Bing Bong NFT to buy it from them for something like 0.85 ETH. Some of them accepted and I would then list the NFT just under the cheapest listed NFT in that collection, in this example something like 0.99 ETH, pocketing 0.14 ETH when I sold it. With the current value of ETH being what it is, this is a nice profit for running a program and clicking a few buttons.

Collection offers

It is 2023 now however and most NFT marketplaces have realized this asymetry and implemented a new feature: the collection offer. The concept is still the same but now you can place offers on entire collections instead of just single NFTs. Here’s an illustration I made to represent the strategy visually:

Collection offer strategy

The yellow zone in the middle is where we make profit, in this example the max profit (difference between highest buy offer and floor price) is 1 ETH.

It is now also much harder to get approved for an OpenSea API key. So I decided to try my luck with another marketplace for which the API would be easier to access. I came upon the MagicEden marketplace for the Solana blockchain which also had the collection offer feature.

Designing the bot

I wanted to improve upon my bot from 2021 and make this one fully automated. I wanted to be able to just run a program on my laptop and have it go make money for me. So from the start I knew I would need the following features:

  1. Get data for a set of NFT collections I was interested in trading - things like trading volume, current floor price, value of the current highest collection offer. This is the data my bot would use to know how much to buy and sell NFTs for.
  2. Create collection offers for collections that have potential for profit and adjust these offers when outbid by the competition or when the collection stops being profitable.
  3. Have a sense of NFTs currently held in the account.
  4. List these NFTs at a profit and adjust the price if other users list NFTs for cheaper than us. It’s important to sell fast to stay liquid and avoid hanging on to any single NFT for too long. Otherwise, we risk a big loss if the value of the NFT we are holding drops.
  5. Provide an interface for tracking the actions taken and the profit realized by the bot (letting a program manage your crypto without supervision is a terrible idea!).

Digging into the API

First, I went and checked out the MagicEden API docs. Before wasting time developping my features, I wanted to see if the API provided some useful endpoints that would save me time trying to scrape data from the site. Upon inspection of the docs, I found these endpoints which provided some of the functionality needed for my bot:

  • api-mainnet.magiceden.dev/v2/collections/:symbol - for getting important data for a collection including the floor price. It’s not explicitly listed in the docs but it’s easy to deduce as three of its sub-endpoints are listed in the docs.
  • api-mainnet.magiceden.dev/v2/wallets/:wallet_address/tokens - for getting the tokens currently held in my account, useful for knowing when someone sells an NFT to my bot.
  • api-mainnet.magiceden.dev/v2/instructions/sell - for generating Solana transaction data to list an NFT to sell on MagicEden. This one however requires an API key to use (which I don’t have).
  • api-mainnet.magiceden.dev/v2/instructions/sell_cancel - for generating Solana transaction data to delist a previously listed NFT. Once again this one requires an API key.

There’s some more endpoints I found in those docs and ended up using but I won’t mention them here as they’re beyond the scope of what I’ll discuss in this writeup. I was still missing some of the data my bot would need to work so I tried finding some private API endpoints not listed in the docs!

So I went to the MagicEden site, navigated to the pages that contained the data, opened Chrome Dev Tools on the Network tab, refreshed the site and inspected the requests made by my browser. This method allowed me to discover these additional endpoints:

  • https://api-mainnet.magiceden.dev/v2/mmm/pools?collectionSymbol=:symbol - for getting collection offers for a given collection. Great for getting the current highest collection offer. Additionally this endpoint was completely accessible when I tried a request from Insomnia!
  • https://api-mainnet.magiceden.io/v2/instructions/mmm/create-pool - for generating Solana transaction data to place a collection offer. This endpoint is on the magiceden.io domain as opposed to the previous endpoints which are all on the magiceden.dev domain. When I tried requesting this endpoint in Insomnia, I got a 403 Forbidden error. I will discuss how I got around this limitation later in the post.
  • https://api-mainnet.magiceden.io/v2/instructions/mmm/sol-withdraw-buy - for generating Solana transaction data to cancel a collection offer. As with the previous endpoint, this one was also protected.

With these endpoints in hand I was ready to get started.

Implementing the API in Ruby

The first thing I did was create a Ruby class called MagicEdenApi. Then I added class methods to it for each API endpoint I planned to call. This would make it easier to query the API in my program. Here’s an example for the best offer endpoint:

class MagicEdenApi
  @@api_endpoint = "https://api-mainnet.magiceden.dev/v2"

  # The method takes a collection symbol as an argument

  def self.get_collection_best_offer(symbol)

    # Make a GET request to the API

    response = RestClient.get("#{@@api_endpoint}/mmm/pools
    ?collectionSymbol=#{symbol}
    &limit=500
    &offset=0
    &filterOnSide=1
    &hideExpired=true
    &direction=1
    &field=5")
    parsed = JSON.parse(response.body)['results']

    # Sleep for 2 seconds to avoid spamming and getting blacklisted from API

    sleep 2

    # Sort offers by price and return relevant information for the highest one

    sorted_offers = parsed.sort_by{|o| o['spotPrice']}.reverse
    if parsed.any?
      {
        best_offer: from_lamports(sorted_offers.first['spotPrice']),
        best_offer_date: sorted_offers.first['updatedAt'],
        best_offerer: sorted_offers.first['poolOwner']
      }
    else
      {}
    end
  end
end

Then now in the rest of my program I could use:

require_relative 'magic_eden_api'

# Get best offer for the Bing Bong collection

MagicEdenApi.get_collection_best_offer('bing_bong')

Now you may be wondering how I went about implementing the API endpoints that weren’t accessible outside of the magiceden.io domain. For these, I wrote a special method in my MagicEdenApi class which I called bypass_api. I figured that when using the site in my browser I was able to access these endpoints so I decided to reproduce the same configuration programatically. Selenium is a framework for automating your browser, so I wrote the following method using Selenium:

def self.bypass_api(url)
  script = "fetch(#{url}).then(response => response.json()).then((data) => { document.body.insertAdjacentHTML('beforeend', `<div id='api-response'>${JSON.stringify(data)}</div>`); });"
  Headless.ly do
    driver = Selenium::WebDriver.for :firefox
    driver.get "https://magiceden.io"
    driver.execute_script(script)
    sleep 5
    begin
      response = JSON.parse(driver.find_element(id: "api-response").text)
    rescue => e
      puts "Error while bypassing api"
      puts e
    end
    driver.quit
    response
  end
end

Put simply here’s what this method does:

  1. It opens Firefox with Selenium
  2. It visits the https://magiceden.io URL
  3. It executes a JavaScript script on the page
  4. That script fetches the endpoint we want to access and inserts the response on the page
  5. After 5 seconds we ask Selenium to find the element that should have been inserted, if it has we return the API response contained in that element

The reason that this method works is that from the perspective of the server, we are using the site in a normal way: browsing it then making normal calls to the API within the same browser. It’s not 100% effective but it’s good enough for our use. Now we can use all the endpoints we found earlier.

Sending transactions to the Solana blockchain

With the features of the API now fully implemented as a Ruby class, we are still missing one key aspect before we go and start buying and selling NFTs. We still need a way to take the transactions we generate with the MagicEden API, sign them, and send them to the Solana blockchain. Remember there are four types of transactions we might want to do:

  • Create a collection offer
  • Delete a collection offer
  • Create a listing for an NFT
  • Delete a listing

For signing and sending Solana transactions, the go to is the @solana/web3.js package in Javascript. Since I’d decided to write most of my app in Ruby, for this small utility layer I decided to go with a dockerized instance of Node.js with an Express web server for communication with my main app. Here’s the process I settled on:

  1. Use the API implementation in Ruby to generate the transaction
  2. Store the unsigned transaction in the database
  3. Request the signing and sending of the transaction by passing the ID of the newly created transaction to the Express web server
  4. Let the docker container take care of handling the transaction and store relevant outputs (like the transaction signature) in the database under the same ID

This is what it looks like for creating a collection offer:

def collection_buy_offer(attributes={})

  # Generate the unsigned buy offer transaction and store it

  buy_offer_unsigned = MagicEdenApi.get_collection_buy_offer(attributes)
  buy_offer = BuyOffer.create(
    unsigned_creation: buy_offer_unsigned,
    status: :unsigned,
    collection: Collection.find_by(symbol: attributes[:symbol]),
    spot_price: attributes[:price])

  sleep 1

  # Make request for docker container to sign and send it

  RestClient.get "http://localhost:1500/buyoffer/create/#{buy_offer.id}"
end

And here’s the method that ends up getting called in the docker container:

const collectionBuyOffer = (transactionId) => {
  client.query(
    "SELECT * FROM buy_offers WHERE id = $1",
    [transactionId],
    (err, res) => {
      const transaction = Web3.Transaction.from(Buffer.from(JSON.parse(res.rows[0].unsigned_creation)));
      const poolAddress = transaction._message.accountKeys[2].toString();
      transaction.partialSign(keypair);
      const serialized = transaction.serialize({requireAllSignatures: true, verifySignatures: true});
      connection.sendEncodedTransaction(serialized.toString('base64'))
        .then((signature) => {
            client.query(
              "UPDATE buy_offers SET signature_creation = $1, pool_address = $2, status = 'active' WHERE id = $3",
              [signature, poolAddress, transactionId])
         })
        .catch((err) => {
          client.query(
              "UPDATE buy_offers SET status = 'invalid' WHERE id = $1",
              [transactionId])
        });
    }
  );
};

The other three transaction types we listed above are implemented in a similar way.

The trading logic

Now with all the tooling in place required to interact with the MagicEden marketplace, we can put it all together to define how our bot trades. I settled on having two main loops that would run continuously. The first one we’ll call the data fetcher and is responsible for constantly refreshing the data of each of the ~100 collections I want to trade on. The second we’ll call the main bot and handles all other tasks we want to do.

The data fetcher is self-explanatory, it updates the data (volume, floor price, best offer) for the first collection, then the second and so on. Once it has updated all collections, it starts over updating the first collection.

The main bot on the other hand does a few things. Here’s what the code looks like for that loop.

loop do
  get_tokens

  @transaction_controller.get_transactions
  puts "Got transactions"
  
  @nft_controller.all_held.each do |nft|
    @collection_controller.pull_data_from_api(nft.collection)
  end
  sell_nfts

  @buy_offer_controller.update_collection_offer_prices
  puts "Updated collection offer prices"

  refresh_active
  place_profitable_offers
  @buy_offer_controller.update_failed_offers
  @buy_offer_controller.cancel_old_offers
end

I’ll now go over what each line in that loop does, skipping over the boring details to give you an overview of how the bot is trading.

  1. The get_tokens method queries the MagicEden API to get the current tokens held in our account. Some of these tokens we may have been holding for a while and already exist in our database, for these tokens we do nothing. For the new tokens that have appeared in our account however, we create a corresponding instance in our database with all the relevant information (the collection it belongs to, the image URL of the token…)
  2. The get_transactions method checks for tokens we may have recently sold. If there are any, that method figures out how much we bought the token for, how much we sold it for, and stores that information in the database. This is what we use to calculate our profit.
  3. The all_held.each loop iterates over each NFT we are currently holding and refreshes the data of the collections those NFTs belong to. This part is important because when we list our NFTs for sale, we want to make sure we are choosing our price based on the latest data available without having to wait for the data fetcher to update that specific collection.
  4. The sell_nfts does a couple things. First it takes all NFTs that are not currently listed and lists them just under the current floor price for that collection (good thing we just updated the data!). Then for NFTs that are already listed, it checks whether the floor price in that collection has fallen below what we listed ours at and if so, it deletes that listing and creates a new one just under the new floor price. This ensures we sell our NFTs quickly by always offering the cheapest one in the collection.
  5. The update_collection_offer_prices I’ll skip over, it’s a method I added after running into bugs sometimes when trying to cancel collection offers.
  6. The refresh_active method is where it starts to get interesting. This method looks at all the collection offers we have placed, then it refreshes the data of the corresponding collections. Finally, if there is an offer higher than ours, it deletes our current offer and places another one just above the competitor who outbid us.
  7. Lastly, the place_profitable_offers method does a few things:
    • It takes the list of all the collections we are tracking and filters them according to the following criteria
    • A minimum collection volume, under which we consider collections not liquid enough to trade (I set this to 50k SOL)
    • A minimum and maximum floor price, this ensures we are targetting NFTs that will both give us a good return but not lock up all our funds in a single big offer (I have this set from 5 SOL to 30 SOL for my bot which is trading with around 100 SOL)
    • A minimum and maximum spread, the spread is the relative size of the gap between the highest buy offer and the floor price. For a collection with a best offer of 9 SOL and a floor price of 10 SOL, the value of the best offer relative to the floor price is 90% so the spread is 10% or 0.1. The higher the spread, the more profit we can make, so why bother setting a max spread? I did this to remove “too good to be true” collections. I set the max spread to 0.5 because if a given collection meets my other criteria and has a 50% spread, my bot has probably made a mistake somewhere along the way and I shouldn’t trade it.
    • Once it has identified a list of “profitable collections” with favorable spreads, it proceeds with placing collection offers on each of those at a value just above the current best offer.
  8. The last two lines I added for redundancy as sometimes the bot would not behave as expected, adding these helped to make the bot run more smoothly

The dashboard

An important part of the development process was to have access to visual feedback of what the bot was doing. For this purpose I implemented a simple Sinatra view displaying all the details I needed. Here’s what that ended up looking like:

Bot screenshot

The main elements of the interface are:

  • Some basic stats like profit and sales
  • A list of recent trades detailing how much each of them gained or lost
  • A list of NFTs currently held by my bot
  • A list of all the active collection offers placed by my bot