Finding top collections by realized gain

In this tutorial, we'll demonstrate how to build an NFT collection discovery engine that surfaces the top NFT collections ranked by their trading volumes and realized gains.

In the code examples we'll be using Golang, so as a prerequisite, you should already have Golang installed on your machine.

Definition

It can be nerve-wracking to watch your portfolio's value drop -- and thrilling to watch it soar. But the important thing to remember is that you don't actually make or lose money until you sell your investments. When you sell an asset, your gain or loss becomes realized, and you either make or lose money on your original investment. By contrast, unrealized gains and losses only exist "on paper"; they're not real yet, because you haven't made a transaction.

Therefore, realized gains and losses occur when you actually sell or dispose of an NFT. Unrealized gains and losses happen when an NFT’s value changes after it is purchased and hasn't yet been sold.

The realized gain of an NFT is calculated by the difference between the NFTs purchase price and sale price. A negative realized gain is usually called a realized loss, but for simplicity in this guide we will use only the term realized gain which can be either positive or negative.

To understand better how a collection is performing in terms of the realized gains or losses for the community of owners as a whole, one way is to compute the total realized gain. The total realized gain is the sum of each individual NFT’s realized gain in that collection. This is not the only way, but it’s a good start.

A collection where each individual collector has only accrued realized gains and no losses is performing really well, but if half of the collectors had negative gains or losses then on the aggregate the total could be equal to 0.

To compute the total realized gain relative to a given collection over a period of 30 days, you need to collect all NFTs sold within that time frame and for each individual owner obtain the price they payed for that NFT. This can be sometime in the past. Then you need to calculate the difference in the price for each of those NFTs and finally compute the sum of all of the individual differences.

From here, you can easily compute the average and the median realized gain, and understand what has been on average the experience for the portion of owners trading the NFT collection in the last 30 days.

Guide prerequisites

  • Golang 1.7+ installed in your environment.
  • Mnemonic API key.

Step 1: Get the top collections by sales volume

Let's first get the list of top collections by the sales volume in the last 30 days.

In order to do that, we are going to use /collections/v1beta1/top/by_sales_volume endpoint.

Copy
Copied
  type Collections struct {
    Collections []*CollectionItem
  }

  type CollectionItem struct {
    ContractAddress string `json:"contractAddress"`
    SalesVolume     string `json:"salesVolume"`
  }

  func getTopCollectionsBySalesVolume() (*Collections, error) {
    // Setup HTTP client.
    var client http.Client

    // Top 10 collections by sales volume in the last 30 days.
    endpoint := "https://ethereum.rest.mnemonichq.com/collections/v1beta1/top/by_sales_volume?limit=10&duration=DURATION_30_DAYS"

    // Create a request.
    req, err := http.NewRequest("GET", endpoint, nil)
    if err != nil {
      return nil, err
    }

    // Set you API key to make the request.
    req.Header.Add("X-API-Key", "<API-KEY>")

    // Execute the request

    ...

    // Unmarshall the result.
    var collections *Collections
    if err := json.Unmarshal(body, &collections); err != nil {
      return nil, err
    }

    return collections, nil
  }

At the time of writing we got the following result.

Note, all returned values are converted and normalized in ETH.

Copy
Copied
{
  "collections": [
    {
      "contractAddress": "0x439cac149b935ae1d726569800972e1669d17094",
      "salesVolume": "1236767.45283261665588597866"
    },
    {
      "contractAddress": "0x4e1f41613c9084fdb9e34e11fae9412427480e56",
      "salesVolume": "537114.513004226653948342300003"
    },
    {
      "contractAddress": "0x7bd29408f11d2bfc23c34f18275bbf23bb716bc7",
      "salesVolume": "247738.34583034111528836974"
    },
    {
      "contractAddress": "0x5cc5b05a8a13e3fbdb0bb9fccd98d38e50f90c38",
      "salesVolume": "137685.04067054385107595262"
    },
    {
      "contractAddress": "0xce25e60a89f200b1fa40f6c313047ffe386992c3",
      "salesVolume": "108121.53232065731976646087"
    },
    {
      "contractAddress": "0x892848074ddea461a15f337250da3ce55580ca85",
      "salesVolume": "71206.21829506112962113264"
    },
    {
      "contractAddress": "0x1dfe7ca09e99d10835bf73044a23b73fc20623df",
      "salesVolume": "70086.9244441221350133654514187743457448"
    },
    {
      "contractAddress": "0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb",
      "salesVolume": "66986.846635277316068119"
    },
    {
      "contractAddress": "0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d",
      "salesVolume": "49083.34151074760768369728"
    },
    {
      "contractAddress": "0x50f5474724e0ee42d9a4e711ccfb275809fd6d4a",
      "salesVolume": "48494.90646967904617448677"
    }
  ]
}

Step 2: Obtain tokens sales for each collection

In order to compute realized gain, as mentioned in the definition, we gonna need to look at every token that has been sold in the last 30 days in each collection.

Mnemonic API allows to obtain a list of transfers for a specified time period per collection, which also includes sale information.

Use the following structs as a reference for the transfers model that is returned from Mnemonic API.

Copy
Copied
    type Transfers struct {
      Transfers []Transfer `json:"transfers"`
    }

    // BlockchainEvent holds the location information of the
    // event on the blockchain.
    type BlockchainEvent struct {
      TxHash   string `json:"txHash"`
      LogIndex string `json:"logIndex"`
      SeqIndex string `json:"seqIndex"`
    }

    // TxValue holds the normalized value of the transaction.
    type TxValue struct {
      Value        string `json:"value"`
      DecimalValue string `json:"decimalValue"`
    }

    // FungibleTransfer holds information about a payment for
    // an NFT made with a fungible token within the same transaction.
    type FungibleTransfer struct {
      BlockTimestamp  time.Time       `json:"blockTimestamp"`
      ContractAddress string          `json:"contractAddress"`
      ContractSymbol  string          `json:"contractSymbol"`
      FromAddress     string          `json:"fromAddress"`
      ToAddress       string          `json:"toAddress"`
      Value           Value           `json:"value"`
      BlockchainEvent BlockchainEvent `json:"blockchainEvent"`
    }

    // Value holds a normalized value of the ERC20 transfer,
    // which is considered as payment for the NFT.
    type Value struct {
      Value        string `json:"value"`
      DecimalValue string `json:"decimalValue"`
      NativeValue  string `json:"nativeValue"`
    }

    type Transfer struct {
      BlockTimestamp       time.Time          `json:"blockTimestamp"`
      ContractAddress      string             `json:"contractAddress"`
      TokenID              string             `json:"tokenId"`
      TransferType         string             `json:"transferType"`
      FromAddress          string             `json:"fromAddress"`
      ToAddress            string             `json:"toAddress"`
      Quantity             string             `json:"quantity"`
      BlockchainEvent      BlockchainEvent    `json:"blockchainEvent"`
      TxValue              TxValue            `json:"txValue"`
      FungibleTransfers    []FungibleTransfer `json:"fungibleTransfers"`
      NftTransfersQuantity string             `json:"nftTransfersQuantity"`
      OperatorAddress      string             `json:"operatorAddress"`
    }

Implement the code to obtain and filter transfers to include only those tokens that were sold in the last 30 days.

Copy
Copied
    // 30 days ago (+1 day due to the greater comparison).
    since := time.Now().Add(-time.Hour * 24 * 31)

    // Iterate over collections and get transfers.
    for _, c := collections {

        // Transfers per collection in the last 30 days (excluding mints and burns).
        endpoint := fmt.Sprintf(
            "https://ethereum.rest.mnemonichq.com/events/v1beta1/transfers/%s?blockTimestampGt=%s&transferTypes=TRANSFER_TYPE_REGULAR",
            c.ContractAddress,
            since.Format("2006-01-02T15:04:05.999999999Z07:00"))

        // Execute query and unmarshall transfers.
        var transfers *Transfers
        ...

        // Store token IDs that have been sold in the last 30 days.
        tokens := make(map[string]bool)

        // Iterate over transfers to find sales (we exclude bundles and swaps in this example for simplicity).
        for _, t := range transfers.Transfers {

            // Parse normalized transaction value.
            txVal, err := strconv.ParseFloat(t.TxValue.DecimalValue, 64)
            if err := nil {
                // Handle error.
            }

            // Sum up all normalized fungible values (if any).
            var fungibleSum float64
            for _, ft := range t.FungibleTransfers {

                // Value of the ERC20 token converted to the native blockchain token (ETH)
                // by the exchange rate effective on the related block minting timestamp.
                natVal, err := strconv.ParseFloat(ft.Value.NativeValue, 64)
                if err != nil {
                    // Handle error.
                }

                fungibleSum += natVal
            }

            // Keep only sales.
            if txVal + fungibleSum > 0 {
                tokens[t.TokenID] = true
            }
        }
    }

Step 3: Compute realized gain per collection

Now that you have a list of tokens for each collection that was sold in the last 30 days, you can compute realized gain per collection by comparing sales of each token within that period.

For simplicity in this tutorial we are going to use just the last two sale events for each token to compute the gain.

For each token that you saved in the previous step use transfers endpoing again, however this time, filtering by the token ID.

Copy
Copied
    // Store total collection realized gain (assuming values are normalized).
    var totalGain float64

    // For each collection iterate over tokens and get transfer events.
    for tokenID, _ := range tokens {

        // Transfers per token in the last 30 days (excluding mints and burns).
        endpoint := fmt.Sprintf(
            "https://ethereum.rest.mnemonichq.com/events/v1beta1/transfers/%s/%s?blockTimestampGt=%s&transferTypes=TRANSFER_TYPE_REGULAR&sortDirection=SORT_DIRECTION_DESC",
            c.ContractAddress,
            tokenID,
            since.Format("2006-01-02T15:04:05.999999999Z07:00"))

        // Execute query and unmarshall transfers.
        var transfers *Transfers
        ...

        var sales []float64
        // Similar to the above, find only last two sales this time.
        for _, t := range transfers.Transfers {
            // Filter non-sale events and store only recent two sales in `sales` (for simplicity).
            // Compute the sum of normalized values of `TxValue` and sum of `FungibleTransfer` values for each.
            ...
        }

        // Compute the difference and add to the total collection realized gain.
        if len(sales) == 2 {
          totalGain += sales[0] - sales[1]
        }
    }

Now that you computed realized gain for each collection in the steps above, it is time to sort your collections by the total realized gain value.

Congrats! You just learned a non-trivial but powerful way of discovering and ranking collections by the realized gain using Mnemonic API. 🎉

After you completed this tutorial, let us know about your experience or if you have any feedback by tagging us on Twitter @mnemonichq or reaching out directly to our support@mnemonichq.com!