Repository: trustwallet/blockatlas
Pull request #1015 (2020-04-07 09:19:54)

Refactor API and define error response


Patch:
diff --git a/api/api.go b/api/api.go
new file mode 100644
index 0000000..22c4ea9
--- /dev/null
+++ b/api/api.go
@@ -0,0 +1,20 @@
+package api
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/platform"
+)
+
+func SetupPlatformAPI(root gin.IRouter) {
+	for _, api := range platform.Platforms {
+		RegisterCollectionsAPI(root, api)
+		RegisterTransactionsAPI(root, api)
+		RegisterCustomAPI(root, api)
+		RegisterTokensAPI(root, api)
+		RegisterStakeAPI(root, api)
+	}
+
+	RegisterBatchAPI(root)
+	RegisterDomainAPI(root)
+	RegisterBasicAPI(root)
+}
diff --git a/api/collection.go b/api/collection.go
deleted file mode 100644
index 9d1081f..0000000
--- a/api/collection.go
+++ /dev/null
@@ -1,287 +0,0 @@
-package api
-
-import (
-	"github.com/gin-gonic/gin"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"github.com/trustwallet/blockatlas/pkg/ginutils"
-	"github.com/trustwallet/blockatlas/platform"
-	"strconv"
-)
-
-// @Summary Get Collections
-// @ID collections_v2
-// @Description Get all collections from the address
-// @Accept json
-// @Produce json
-// @Tags Collections
-// @Param coin path string true "the coin name" default(ethereum)
-// @Param address path string true "the query address" default(0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB)
-// @Success 200 {object} blockatlas.CollectionPage
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v2/{coin}/collections/{address} [get]
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func oldMakeCollectionsRoute(router gin.IRouter, api blockatlas.Platform) {
-	var collectionAPI blockatlas.CollectionAPI
-	collectionAPI, _ = api.(blockatlas.CollectionAPI)
-
-	if collectionAPI == nil {
-		return
-	}
-
-	router.GET("/collections/:owner", func(c *gin.Context) {
-		collections, err := collectionAPI.OldGetCollections(c.Param("owner"))
-		if err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		ginutils.RenderSuccess(c, collections)
-	})
-}
-
-// @Summary Get Collections
-// @ID collections_v3
-// @Description Get all collections from the address
-// @Accept json
-// @Produce json
-// @Tags Collections
-// @Param coin path string true "the coin name" default(ethereum)
-// @Param address path string true "the query address" default(0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB)
-// @Success 200 {object} blockatlas.CollectionPage
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v3/{coin}/collections/{address} [get]
-func makeCollectionsRoute(router gin.IRouter, api blockatlas.Platform) {
-	var collectionAPI blockatlas.CollectionAPI
-	collectionAPI, _ = api.(blockatlas.CollectionAPI)
-
-	if collectionAPI == nil {
-		return
-	}
-
-	router.GET("/collections/:owner", func(c *gin.Context) {
-		collections, err := collectionAPI.GetCollections(c.Param("owner"))
-		if err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		ginutils.RenderSuccess(c, collections)
-	})
-}
-
-// @Summary Get Collection
-// @ID collection_v2
-// @Description Get a collection from the address
-// @Accept json
-// @Produce json
-// @Tags Collections
-// @Param coin path string true "the coin name" default(ethereum)
-// @Param owner path string true "the query address" default(0x0875BCab22dE3d02402bc38aEe4104e1239374a7)
-// @Param collection_id path string true "the query collection" default(0x06012c8cf97bead5deae237070f9587f8e7a266d)
-// @Success 200 {object} blockatlas.CollectionPage
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v2/{coin}/collections/{owner}/collection/{collection_id} [get]
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func oldMakeCollectionRoute(router gin.IRouter, api blockatlas.Platform) {
-	var collectionAPI blockatlas.CollectionAPI
-	collectionAPI, _ = api.(blockatlas.CollectionAPI)
-
-	if collectionAPI == nil {
-		return
-	}
-
-	router.GET("/collections/:owner/collection/:collection_id", func(c *gin.Context) {
-		collectibles, err := collectionAPI.OldGetCollectibles(c.Param("owner"), c.Param("collection_id"))
-		if err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		ginutils.RenderSuccess(c, collectibles)
-	})
-}
-
-// @Summary Get Collection
-// @ID collection_v3
-// @Description Get a collection from the address
-// @Accept json
-// @Produce json
-// @Tags Collections
-// @Param coin path string true "the coin name" default(ethereum)
-// @Param owner path string true "the query address" default(0x0875BCab22dE3d02402bc38aEe4104e1239374a7)
-// @Param collection_id path string true "the query collection" default(0x06012c8cf97bead5deae237070f9587f8e7a266d)
-// @Success 200 {object} blockatlas.CollectionPage
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v3/{coin}/collections/{owner}/collection/{collection_id} [get]
-func makeCollectionRoute(router gin.IRouter, api blockatlas.Platform) {
-	var collectionAPI blockatlas.CollectionAPI
-	collectionAPI, _ = api.(blockatlas.CollectionAPI)
-
-	if collectionAPI == nil {
-		return
-	}
-
-	router.GET("/collections/:owner/collection/:collection_id", func(c *gin.Context) {
-		collectibles, err := collectionAPI.GetCollectibles(c.Param("owner"), c.Param("collection_id"))
-		if err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		ginutils.RenderSuccess(c, collectibles)
-	})
-}
-
-// @Description Get collection categories
-// @ID collection_categories_v2
-// @Summary Get list of collections from a specific coin and addresses
-// @Accept json
-// @Produce json
-// @Tags Collections
-// @Param data body string true "Payload" default({"60": ["0xb3624367b1ab37daef42e1a3a2ced012359659b0"]})
-// @Success 200 {object} blockatlas.DocsResponse
-// @Router /v2/collectibles/categories [post]
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func oldMakeCategoriesBatchRoute(router gin.IRouter) {
-	router.POST("/collectibles/categories", func(c *gin.Context) {
-		var reqs map[string][]string
-		if err := c.BindJSON(&reqs); err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		batch := make(blockatlas.CollectionPage, 0)
-		for key, addresses := range reqs {
-			coinId, err := strconv.Atoi(key)
-			if err != nil {
-				continue
-			}
-			p, ok := platform.CollectionAPIs[uint(coinId)]
-			if !ok {
-				continue
-			}
-			for _, address := range addresses {
-				collections, err := p.OldGetCollections(address)
-				if err != nil {
-					continue
-				}
-				batch = append(batch, collections...)
-			}
-		}
-		ginutils.RenderSuccess(c, batch)
-	})
-}
-
-// @Description Get collection categories
-// @ID collection_categories_v3
-// @Summary Get list of collections from a specific coin and addresses
-// @Accept json
-// @Produce json
-// @Tags Collections
-// @Param data body string true "Payload" default({"60": ["0xb3624367b1ab37daef42e1a3a2ced012359659b0"]})
-// @Success 200 {object} blockatlas.DocsResponse
-// @Router /v3/collectibles/categories [post]
-func makeCategoriesBatchRoute(router gin.IRouter) {
-	router.POST("/collectibles/categories", func(c *gin.Context) {
-		var reqs map[string][]string
-		if err := c.BindJSON(&reqs); err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		batch := make(blockatlas.CollectionPage, 0)
-		for key, addresses := range reqs {
-			coinId, err := strconv.Atoi(key)
-			if err != nil {
-				continue
-			}
-			p, ok := platform.CollectionAPIs[uint(coinId)]
-			if !ok {
-				continue
-			}
-			for _, address := range addresses {
-				collections, err := p.GetCollections(address)
-				if err != nil {
-					continue
-				}
-				batch = append(batch, collections...)
-			}
-		}
-		ginutils.RenderSuccess(c, batch)
-	})
-}
-
-// @Description Get collection categories
-// @ID collection_categories_v4
-// @Summary Get list of collections from a specific coin and addresses
-// @Accept json
-// @Produce json
-// @Tags Collections
-// @Param data body string true "Payload" default({"60": ["0xb3624367b1ab37daef42e1a3a2ced012359659b0"]})
-// @Success 200 {object} blockatlas.DocsResponse
-// @Router /v4/collectibles/categories [post]
-func makeCategoriesBatchRouteV4(router gin.IRouter) {
-	router.POST("/collectibles/categories", func(c *gin.Context) {
-		var reqs map[string][]string
-		if err := c.BindJSON(&reqs); err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		batch := make(blockatlas.CollectionPage, 0)
-		for key, addresses := range reqs {
-			coinId, err := strconv.Atoi(key)
-			if err != nil {
-				continue
-			}
-			p, ok := platform.CollectionAPIs[uint(coinId)]
-			if !ok {
-				continue
-			}
-			for _, address := range addresses {
-				collections, err := p.GetCollectionsV4(address)
-				if err != nil {
-					continue
-				}
-				batch = append(batch, collections...)
-			}
-		}
-		ginutils.RenderSuccess(c, batch)
-	})
-}
-
-// @Summary Get Collection
-// @ID collection_v4
-// @Description Get a collection from the address
-// @Accept json
-// @Produce json
-// @Tags Collections
-// @Param coin path string true "the coin name" default(ethereum)
-// @Param owner path string true "the query address" default(0x0875BCab22dE3d02402bc38aEe4104e1239374a7)
-// @Param collection_id path string true "the query collection" default(0x06012c8cf97bead5deae237070f9587f8e7a266d)
-// @Success 200 {object} blockatlas.CollectionPage
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v4/{coin}/collections/{owner}/collection/{collection_id} [get]
-func makeCollectionRouteV4(router gin.IRouter, api blockatlas.Platform) {
-	var collectionAPI blockatlas.CollectionAPI
-	collectionAPI, _ = api.(blockatlas.CollectionAPI)
-
-	if collectionAPI == nil {
-		return
-	}
-
-	router.GET("/collections/:owner/collection/:collection_id", func(c *gin.Context) {
-		collectibles, err := collectionAPI.GetCollectiblesV4(c.Param("owner"), c.Param("collection_id"))
-		if err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		ginutils.RenderSuccess(c, collectibles)
-	})
-}
-
-func emptyPage(c *gin.Context) {
-	var page blockatlas.TxPage
-	ginutils.RenderSuccess(c, &page)
-}
diff --git a/api/domain.go b/api/domain.go
deleted file mode 100644
index 7c720db..0000000
--- a/api/domain.go
+++ /dev/null
@@ -1,95 +0,0 @@
-package api
-
-import (
-	"net/http"
-	"strconv"
-	"strings"
-
-	"github.com/trustwallet/blockatlas/pkg/errors"
-	"github.com/trustwallet/blockatlas/services/domains"
-
-	"github.com/trustwallet/blockatlas/pkg/ginutils"
-
-	"github.com/gin-gonic/gin"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-)
-
-type LookupBatchPage []blockatlas.Resolved
-
-// @Summary Lookup .eth / .zil addresses
-// @ID lookup
-// @Description Lookup ENS/ZNS to find registered addresses
-// @Produce json
-// @Tags Naming
-// @Param name query string empty "string name"
-// @Param coin query string 60 "string coin"
-// @Success 200 {object} blockatlas.Resolved
-// @Failure 500 {object} ginutils.ApiError
-// @Router /ns/lookup [get]
-func MakeLookupRoute(router gin.IRouter) {
-	router.GET("/lookup", func(c *gin.Context) {
-		name := c.Query("name")
-		coinQuery := c.Query("coin")
-		coin, err := strconv.ParseUint(coinQuery, 10, 64)
-		if err != nil {
-			ginutils.RenderError(c, http.StatusBadRequest, "coin query is invalid")
-			return
-		}
-
-		result, err := domains.HandleLookup(name, []uint64{coin})
-		if err != nil {
-			ginutils.RenderError(c, http.StatusBadRequest, err.Error())
-			return
-		}
-		if len(result) == 0 {
-			ginutils.RenderError(c, http.StatusBadRequest, errors.E("name not found", errors.Params{"coin": coin, "name": name}).Error())
-			return
-		}
-		ginutils.RenderSuccess(c, result[0])
-	})
-}
-
-// @Summary Lookup .eth / .zil addresses
-// @ID lookup
-// @Description Lookup ENS/ZNS to find registered addresses for multiple coins
-// @Produce json
-// @Tags Naming
-// @Param name query string empty "string name"
-// @Param coins query string true "List of coins"
-// @Success 200 {array} blockatlas.Resolved
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v2/ns/lookup [get]
-func MakeLookupBatchRoute(router gin.IRouter) {
-	router.GET("/lookup", func(c *gin.Context) {
-		name := c.Query("name")
-		coinsRaw := strings.Split(c.Query("coins"), ",")
-		coins, err := sliceAtoi(coinsRaw)
-		if err != nil {
-			ginutils.RenderError(c, http.StatusBadRequest, "coin query is invalid")
-			return
-		}
-
-		result, err := domains.HandleLookup(name, coins)
-		if err != nil {
-			ginutils.RenderError(c, http.StatusBadRequest, err.Error())
-			return
-		}
-		if len(result) == 0 {
-			ginutils.RenderError(c, http.StatusBadRequest, errors.E("name not found", errors.Params{"name": name}).Error())
-			return
-		}
-		ginutils.RenderSuccess(c, result)
-	})
-}
-
-func sliceAtoi(sa []string) ([]uint64, error) {
-	si := make([]uint64, 0, len(sa))
-	for _, a := range sa {
-		i, err := strconv.ParseUint(a, 10, 64)
-		if err != nil {
-			return si, err
-		}
-		si = append(si, i)
-	}
-	return si, nil
-}
diff --git a/api/endpoint/basic.go b/api/endpoint/basic.go
new file mode 100644
index 0000000..dbf68d6
--- /dev/null
+++ b/api/endpoint/basic.go
@@ -0,0 +1,23 @@
+package endpoint
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/internal"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"net/http"
+)
+
+func GetRoot(c *gin.Context) { c.JSON(http.StatusOK, "Welcome to the Block Atlas API") }
+
+func GetStatus(c *gin.Context) {
+	c.JSON(http.StatusOK, map[string]interface{}{
+		"status": true,
+		"build":  internal.Build,
+		"date":   internal.Date,
+	})
+}
+
+func EmptyPage(c *gin.Context) {
+	var page blockatlas.TxPage
+	c.JSON(http.StatusOK, &page)
+}
diff --git a/api/endpoint/collection.go b/api/endpoint/collection.go
new file mode 100644
index 0000000..a7fc385
--- /dev/null
+++ b/api/endpoint/collection.go
@@ -0,0 +1,114 @@
+package endpoint
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/api/model"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"net/http"
+	"strconv"
+)
+
+// @Summary Get Collection
+// @ID collection_v4
+// @Description Get a collection from the address
+// @Accept json
+// @Produce json
+// @Tags Collections
+// @Param coin path string true "the coin name" default(ethereum)
+// @Param owner path string true "the query address" default(0x0875BCab22dE3d02402bc38aEe4104e1239374a7)
+// @Param collection_id path string true "the query collection" default(0x06012c8cf97bead5deae237070f9587f8e7a266d)
+// @Success 200 {object} blockatlas.CollectionPage
+// @Failure 500 {object} middleware.ApiError
+// @Router /v4/{coin}/collections/{owner}/collection/{collection_id} [get]
+func GetCollectiblesForSpecificCollectionAndOwner(c *gin.Context, api blockatlas.CollectionAPI) {
+	collectibles, err := api.GetCollectibles(c.Param("owner"), c.Param("collection_id"))
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, model.CreateErrorResponse(model.InternalFail, err))
+		return
+	}
+	c.JSON(http.StatusOK, &collectibles)
+}
+
+// @Description Get collection categories
+// @ID collection_categories_v4
+// @Summary Get list of collections from a specific coin and addresses
+// @Accept json
+// @Produce json
+// @Tags Collections
+// @Param data body string true "Payload" default({"60": ["0xb3624367b1ab37daef42e1a3a2ced012359659b0"]})
+// @Success 200 {object} blockatlas.DocsResponse
+// @Router /v4/collectibles/categories [post]
+func GetCollectionCategoriesFromList(c *gin.Context, apis map[uint]blockatlas.CollectionAPI) {
+	var reqs map[string][]string
+	if err := c.BindJSON(&reqs); err != nil {
+		c.JSON(http.StatusBadRequest, model.CreateErrorResponse(model.Default, err))
+		return
+	}
+
+	batch := make(blockatlas.CollectionPage, 0)
+	for key, addresses := range reqs {
+		coinId, err := strconv.Atoi(key)
+		if err != nil {
+			continue
+		}
+		p, ok := apis[uint(coinId)]
+		if !ok {
+			continue
+		}
+		for _, address := range addresses {
+			collections, err := p.GetCollections(address)
+			if err != nil {
+				continue
+			}
+			batch = append(batch, collections...)
+		}
+	}
+	c.JSON(http.StatusOK, &batch)
+}
+
+func GetCollectiblesForOwnerV3(c *gin.Context, api blockatlas.CollectionAPI) {
+	collections, err := api.GetCollectionsV3(c.Param("owner"))
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, model.CreateErrorResponse(model.InternalFail, err))
+		return
+	}
+
+	c.JSON(http.StatusOK, &collections)
+}
+
+func GetCollectiblesForSpecificCollectionAndOwnerV3(c *gin.Context, api blockatlas.CollectionAPI) {
+	collectibles, err := api.GetCollectiblesV3(c.Param("owner"), c.Param("collection_id"))
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, model.CreateErrorResponse(model.InternalFail, err))
+		return
+	}
+	c.JSON(http.StatusOK, &collectibles)
+}
+
+func GetCollectionCategoriesFromListV3(c *gin.Context, apis map[uint]blockatlas.CollectionAPI) {
+	var reqs map[string][]string
+	if err := c.BindJSON(&reqs); err != nil {
+		c.JSON(http.StatusBadRequest, model.CreateErrorResponse(model.Default, err))
+		return
+	}
+
+	batch := make(blockatlas.CollectionPageV3, 0)
+	for key, addresses := range reqs {
+		coinId, err := strconv.Atoi(key)
+		if err != nil {
+			continue
+		}
+		p, ok := apis[uint(coinId)]
+		if !ok {
+			continue
+		}
+		for _, address := range addresses {
+			collections, err := p.GetCollectionsV3(address)
+			if err != nil {
+				continue
+			}
+			batch = append(batch, collections...)
+		}
+	}
+	c.JSON(http.StatusOK, &batch)
+}
diff --git a/api/endpoint/domain.go b/api/endpoint/domain.go
new file mode 100644
index 0000000..9c4cf4b
--- /dev/null
+++ b/api/endpoint/domain.go
@@ -0,0 +1,82 @@
+package endpoint
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/api/model"
+	"github.com/trustwallet/blockatlas/services/domains"
+	"net/http"
+	"strconv"
+	"strings"
+)
+
+// @Summary Lookup .eth / .zil addresses
+// @ID lookup
+// @Description Lookup ENS/ZNS to find registered addresses
+// @Produce json
+// @Tags Naming
+// @Param name query string empty "string name"
+// @Param coin query string 60 "string coin"
+// @Success 200 {object} blockatlas.Resolved
+// @Failure 500 {object} middleware.ApiError
+// @Router /ns/lookup [get]
+func GetAddressByCoinAndDomain(c *gin.Context) {
+	name := c.Query("name")
+	coinQuery := c.Query("coin")
+	coin, err := strconv.ParseUint(coinQuery, 10, 64)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, model.CreateErrorResponse(model.InvalidQuery, err))
+		return
+	}
+	result, err := domains.HandleLookup(name, []uint64{coin})
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, model.CreateErrorResponse(model.InternalFail, err))
+		return
+	}
+	if len(result) == 0 {
+		c.JSON(http.StatusNotFound, model.CreateErrorResponse(model.RequestedDataNotFound, err))
+		return
+	}
+	c.JSON(http.StatusOK, result[0])
+}
+
+// @Summary Lookup .eth / .zil addresses
+// @ID lookup
+// @Description Lookup ENS/ZNS to find registered addresses for multiple coins
+// @Produce json
+// @Tags Naming
+// @Param name query string empty "string name"
+// @Param coins query string true "List of coins"
+// @Success 200 {array} blockatlas.Resolved
+// @Failure 500 {object} middleware.ApiError
+// @Router /v2/ns/lookup [get]
+func GetAddressByCoinAndDomainBatch(c *gin.Context) {
+	name := c.Query("name")
+	coinsRaw := strings.Split(c.Query("coins"), ",")
+	coins, err := sliceAtoi(coinsRaw)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, model.CreateErrorResponse(model.InvalidQuery, err))
+		return
+	}
+	result, err := domains.HandleLookup(name, coins)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, model.CreateErrorResponse(model.InternalFail, err))
+		return
+	}
+	if len(result) == 0 {
+		c.JSON(http.StatusNotFound, model.CreateErrorResponse(model.RequestedDataNotFound, err))
+		return
+	}
+	c.JSON(http.StatusOK, &result)
+}
+
+func sliceAtoi(sa []string) ([]uint64, error) {
+	si := make([]uint64, 0, len(sa))
+	for _, a := range sa {
+		i, err := strconv.ParseUint(a, 10, 64)
+		if err != nil {
+			return si, err
+		}
+		si = append(si, i)
+	}
+	return si, nil
+}
diff --git a/api/endpoint/staking.go b/api/endpoint/staking.go
new file mode 100644
index 0000000..fb0acef
--- /dev/null
+++ b/api/endpoint/staking.go
@@ -0,0 +1,168 @@
+package endpoint
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/api/model"
+	"github.com/trustwallet/blockatlas/coin"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/errors"
+	services "github.com/trustwallet/blockatlas/services/assets"
+	"net/http"
+)
+
+type (
+	AddressBatchRequest struct {
+		Address string `json:"address"`
+		CoinBatchRequest
+	}
+
+	CoinBatchRequest struct {
+		Coin uint `json:"coin"`
+	}
+
+	ENSBatchRequest struct {
+		Coins []uint64 `json:"coins"`
+		Name  string   `json:"name"`
+	}
+
+	AddressesRequest []AddressBatchRequest
+	CoinsRequest     []CoinBatchRequest
+)
+
+// @Summary Get Multiple Stake Delegations
+// @ID batch_delegations
+// @Description Get Stake Delegations for multiple coins
+// @Accept json
+// @Produce json
+// @Tags Staking
+// @Param delegations body api.AddressesRequest true "Validators addresses and coins"
+// @Success 200 {object} blockatlas.DelegationsBatchPage
+// @Router /v2/staking/delegations [post]
+func GetStakeDelegationsWithAllInfoForBatch(c *gin.Context, apis map[string]blockatlas.StakeAPI) {
+	var reqs AddressesRequest
+	if err := c.BindJSON(&reqs); err != nil {
+		c.JSON(http.StatusBadRequest, model.CreateErrorResponse(model.InvalidQuery, err))
+		return
+	}
+
+	batch := make(blockatlas.DelegationsBatchPage, 0)
+	for _, r := range reqs {
+		requestCoin, ok := coin.Coins[r.Coin]
+		if !ok {
+			continue
+		}
+		p, ok := apis[requestCoin.Handle]
+		if !ok {
+			continue
+		}
+		delegation, err := getDelegationResponse(p, r.Address)
+		if err != nil {
+			continue
+		}
+		batch = append(batch, delegation)
+	}
+	c.JSON(http.StatusOK, blockatlas.DocsResponse{Docs: &batch})
+}
+
+// @Summary Get Multiple Stake Delegations
+// @ID batch_delegations
+// @Description Get Stake Delegations for multiple coins
+// @Accept json
+// @Produce json
+// @Tags Staking
+// @Param delegations body api.AddressesRequest true "Validators addresses and coins"
+// @Success 200 {object} blockatlas.DelegationsBatchPage
+// @Router /v2/staking/list [post]
+func GetStakeInfoForBatch(c *gin.Context, apis map[string]blockatlas.StakeAPI) {
+	var reqs CoinsRequest
+	if err := c.BindJSON(&reqs); err != nil {
+		c.JSON(http.StatusBadRequest, model.CreateErrorResponse(model.InvalidQuery, err))
+		return
+	}
+
+	batch := make(blockatlas.StakingBatchPage, 0)
+	for _, r := range reqs {
+		requestCoin, ok := coin.Coins[r.Coin]
+		if !ok {
+			continue
+		}
+		p, ok := apis[requestCoin.Handle]
+		if !ok {
+			continue
+		}
+		staking := getStakingResponse(p)
+		batch = append(batch, staking)
+	}
+	c.JSON(http.StatusOK, blockatlas.DocsResponse{Docs: &batch})
+}
+
+// @Summary Get Validators
+// @ID validators
+// @Description Get validators from the address
+// @Accept json
+// @Produce json
+// @Tags Staking
+// @Param coin path string true "the coin name" default(cosmos)
+// @Success 200 {object} blockatlas.DocsResponse
+// @Failure 500 {object} middleware.ApiError
+// @Router /v2/{coin}/staking/validators [get]
+func GetValidators(c *gin.Context, api blockatlas.StakeAPI) {
+	results, err := services.GetActiveValidators(api)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, model.CreateErrorResponse(model.InternalFail, err))
+		return
+	}
+	c.JSON(http.StatusOK, blockatlas.DocsResponse{Docs: &results})
+}
+
+// @Summary Get Stake Delegations
+// @ID delegations
+// @Description Get stake delegations from the address
+// @Accept json
+// @Produce json
+// @Tags Staking
+// @Param coin path string true "the coin name" default(tron)
+// @Param address path string true "the query address" default(TPJYCz8ppZNyvw7pTwmjajcx4Kk1MmEUhD)
+// @Success 200 {object} blockatlas.DelegationResponse
+// @Failure 500 {object} middleware.ApiError
+// @Router /v2/{coin}/staking/delegations/{address} [get]
+func GetStakingDelegationsForSpecificCoin(c *gin.Context, api blockatlas.StakeAPI) {
+	result, err := getDelegationResponse(api, c.Param("address"))
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, model.CreateErrorResponse(model.InternalFail, err))
+		return
+	}
+	c.JSON(http.StatusOK, &result)
+}
+
+func getDelegationResponse(api blockatlas.StakeAPI, address string) (blockatlas.DelegationResponse, error) {
+	delegations, err := api.GetDelegations(address)
+	if err != nil {
+		return blockatlas.DelegationResponse{
+			StakingResponse: getStakingResponse(api),
+			Address:         address,
+		}, errors.E("Unable to fetch delegations list", err)
+	}
+	balance, err := api.UndelegatedBalance(address)
+	if err != nil {
+		return blockatlas.DelegationResponse{
+			Delegations:     delegations,
+			Address:         address,
+			StakingResponse: getStakingResponse(api),
+		}, errors.E("Unable to fetch undelegated balance", err)
+	}
+	return blockatlas.DelegationResponse{
+		Balance:         balance,
+		Delegations:     delegations,
+		Address:         address,
+		StakingResponse: getStakingResponse(api),
+	}, nil
+}
+
+func getStakingResponse(api blockatlas.StakeAPI) blockatlas.StakingResponse {
+	stakingCoin := api.Coin()
+	return blockatlas.StakingResponse{
+		Coin:    stakingCoin.External(),
+		Details: api.GetDetails(),
+	}
+}
diff --git a/api/endpoint/token.go b/api/endpoint/token.go
new file mode 100644
index 0000000..4111c54
--- /dev/null
+++ b/api/endpoint/token.go
@@ -0,0 +1,34 @@
+package endpoint
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/api/model"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"net/http"
+)
+
+// @Summary Get Tokens
+// @ID tokens
+// @Description Get tokens from the address
+// @Accept json
+// @Produce json
+// @Tags Transactions
+// @Param coin path string true "the coin name" default(ethereum)
+// @Param address path string true "the query address" default(0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB)
+// @Success 200 {object} blockatlas.CollectionPage
+// @Failure 500 {object} middleware.ApiError
+// @Router /v2/{coin}/tokens/{address} [get]
+func GetTokensByAddress(c *gin.Context, tokenAPI blockatlas.TokenAPI) {
+	address := c.Param("address")
+	if address == "" {
+		EmptyPage(c)
+		return
+	}
+
+	result, err := tokenAPI.GetTokenListByAddress(address)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, model.CreateErrorResponse(model.InternalFail, err))
+		return
+	}
+	c.JSON(http.StatusOK, blockatlas.DocsResponse{Docs: &result})
+}
diff --git a/api/endpoint/transaction.go b/api/endpoint/transaction.go
new file mode 100644
index 0000000..52ba419
--- /dev/null
+++ b/api/endpoint/transaction.go
@@ -0,0 +1,89 @@
+package endpoint
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/api/model"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/pkg/errors"
+	"net/http"
+	"sort"
+)
+
+// @Summary Get Transactions
+// @ID tx_v2
+// @Description Get transactions from the address
+// @Accept json
+// @Produce json
+// @Tags Transactions
+// @Param coin path string true "the coin name" default(tezos)
+// @Param address path string true "the query address" default(tz1WCd2jm4uSt4vntk4vSuUWoZQGhLcDuR9q)
+// @Success 200 {object} blockatlas.TxPage
+// @Failure 500 {object} middleware.ApiError
+// @Router /v2/{coin}/transactions/{address} [get]
+
+// @Summary Get Transactions
+// @ID tx_v1
+// @Description Get transactions from the address
+// @Accept json
+// @Produce json
+// @Tags Transactions
+// @Param coin path string true "the coin name" default(tezos)
+// @Param address path string true "the query address" default(tz1WCd2jm4uSt4vntk4vSuUWoZQGhLcDuR9q)
+// @Failure 500 {object} middleware.ApiError
+// @Router /v1/{coin}/{address} [get]
+func GetTransactionsHistory(c *gin.Context, txAPI blockatlas.TxAPI, tokenTxAPI blockatlas.TokenTxAPI) {
+	address := c.Param("address")
+	if address == "" {
+		c.JSON(http.StatusBadRequest, model.CreateErrorResponse(model.InvalidQuery, blockatlas.ErrInvalidAddr))
+		return
+	}
+	token := c.Query("token")
+
+	var (
+		txs []blockatlas.Tx
+		err error
+	)
+
+	switch {
+	case token == "" && txAPI != nil:
+		txs, err = txAPI.GetTxsByAddress(address)
+	case token != "" && tokenTxAPI != nil:
+		txs, err = tokenTxAPI.GetTokenTxsByAddress(address, token)
+	default:
+		c.JSON(http.StatusInternalServerError,
+			model.CreateErrorResponse(model.InternalFail, errors.E("Failed to find api for that coin")))
+		return
+	}
+
+	if err != nil {
+		switch err {
+		case blockatlas.ErrInvalidAddr:
+			c.JSON(http.StatusBadRequest,
+				model.CreateErrorResponse(model.InvalidQuery, blockatlas.ErrInvalidAddr))
+			return
+		case blockatlas.ErrNotFound:
+			c.JSON(http.StatusNotFound,
+				model.CreateErrorResponse(model.RequestedDataNotFound, blockatlas.ErrNotFound))
+			return
+		case blockatlas.ErrSourceConn:
+			c.JSON(http.StatusServiceUnavailable,
+				model.CreateErrorResponse(model.InternalFail, blockatlas.ErrSourceConn))
+			return
+		default:
+			c.JSON(http.StatusInternalServerError,
+				model.CreateErrorResponse(model.Default, err))
+			return
+		}
+	}
+
+	page := make(blockatlas.TxPage, 0)
+	for _, tx := range txs {
+		tx.Direction = tx.GetTransactionDirection(address)
+		page = append(page, tx)
+	}
+	if len(page) > blockatlas.TxPerPage {
+		page = page[0:blockatlas.TxPerPage]
+	}
+	sort.Sort(&page)
+	c.JSON(http.StatusOK, &page)
+}
diff --git a/pkg/ginutils/gincache/cache.go b/api/middleware/cache.go
similarity index 97%
rename from pkg/ginutils/gincache/cache.go
rename to api/middleware/cache.go
index 43474e1..efba3bb 100644
--- a/pkg/ginutils/gincache/cache.go
+++ b/api/middleware/cache.go
@@ -1,4 +1,4 @@
-package gincache
+package middleware
 
 import (
 	"bytes"
@@ -43,8 +43,6 @@ type cachedWriter struct {
 	key     string
 }
 
-var _ gin.ResponseWriter = &cachedWriter{}
-
 func newCachedWriter(expire time.Duration, writer gin.ResponseWriter, key string) *cachedWriter {
 	return &cachedWriter{writer, 0, false, expire, key}
 }
@@ -151,7 +149,7 @@ func generateKey(c *gin.Context) string {
 	return base64.URLEncoding.EncodeToString(hash[:])
 }
 
-// CacheMiddleware encapsulates a gin handler function and caches the response with an expiration time.
+// CacheMiddleware encapsulates a gin handler function and caches the model with an expiration time.
 func CacheMiddleware(expiration time.Duration, handle gin.HandlerFunc) gin.HandlerFunc {
 	return func(c *gin.Context) {
 		defer c.Next()
diff --git a/pkg/ginutils/middleware.go b/api/middleware/cors.go
similarity index 51%
rename from pkg/ginutils/middleware.go
rename to api/middleware/cors.go
index 0cc1623..58f48fb 100644
--- a/pkg/ginutils/middleware.go
+++ b/api/middleware/cors.go
@@ -1,36 +1,9 @@
-package ginutils
+package middleware
 
 import (
 	"github.com/gin-gonic/gin"
-	"net/http"
-	"strings"
 )
 
-var DefaultMiddleware = func(c *gin.Context) {
-	c.Next()
-}
-
-func TokenAuthMiddleware(requiredToken string) gin.HandlerFunc {
-	if requiredToken == "" {
-		return DefaultMiddleware
-	}
-
-	return func(c *gin.Context) {
-		token := c.Request.Header.Get("Authorization")
-		token = strings.Replace(token, "Bearer ", "", 1)
-		if token == "" {
-			RenderError(c, http.StatusUnauthorized, "API token required")
-			return
-		}
-
-		if token != requiredToken {
-			RenderError(c, http.StatusUnauthorized, "Invalid API token")
-			return
-		}
-		c.Next()
-	}
-}
-
 func CORSMiddleware() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
diff --git a/pkg/ginutils/reverse_proxy.go b/api/middleware/reverse_proxy.go
similarity index 95%
rename from pkg/ginutils/reverse_proxy.go
rename to api/middleware/reverse_proxy.go
index c828956..868be6b 100644
--- a/pkg/ginutils/reverse_proxy.go
+++ b/api/middleware/reverse_proxy.go
@@ -1,4 +1,4 @@
-package ginutils
+package middleware
 
 import (
 	"github.com/gin-gonic/gin"
diff --git a/api/model/model.go b/api/model/model.go
new file mode 100644
index 0000000..e7708e4
--- /dev/null
+++ b/api/model/model.go
@@ -0,0 +1,31 @@
+package model
+
+const (
+	Default ErrorCode = iota
+	InvalidQuery
+	RequestedDataNotFound
+	InternalFail
+)
+
+type (
+	ErrorResponse struct {
+		Error ErrorDetails `json:"error"`
+	}
+	ErrorDetails struct {
+		Message string    `json:"message"`
+		Code    ErrorCode `json:"code"`
+	}
+
+	ErrorCode int
+)
+
+func CreateErrorResponse(code ErrorCode, err error) ErrorResponse {
+	var message string
+	if err != nil {
+		message = err.Error()
+	}
+	return ErrorResponse{Error: ErrorDetails{
+		Message: message,
+		Code:    code,
+	}}
+}
diff --git a/api/registry.go b/api/registry.go
new file mode 100644
index 0000000..aabe416
--- /dev/null
+++ b/api/registry.go
@@ -0,0 +1,141 @@
+package api
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/api/endpoint"
+	"github.com/trustwallet/blockatlas/coin"
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"github.com/trustwallet/blockatlas/platform"
+)
+
+func RegisterCollectionsAPI(root gin.IRouter, api blockatlas.Platform) {
+	collectionAPI, ok := api.(blockatlas.CollectionAPI)
+	if !ok {
+		return
+	}
+
+	handle := collectionAPI.Coin().Handle
+
+	root.GET("/v3/"+handle+"/collections/:owner/collection/:collection_id", func(c *gin.Context) {
+		endpoint.GetCollectiblesForSpecificCollectionAndOwnerV3(c, collectionAPI)
+	})
+	root.GET("/v3/"+handle+"/collections/:owner", func(c *gin.Context) {
+		endpoint.GetCollectiblesForOwnerV3(c, collectionAPI)
+	})
+	root.GET("/v4/"+handle+"/collections/:owner/collection/:collection_id", func(c *gin.Context) {
+		endpoint.GetCollectiblesForSpecificCollectionAndOwner(c, collectionAPI)
+	})
+}
+
+func RegisterTokensAPI(root gin.IRouter, api blockatlas.Platform) {
+	tokenAPI, ok := api.(blockatlas.TokenAPI)
+	if !ok {
+		return
+	}
+
+	handle := api.Coin().Handle
+
+	root.GET("/v2/"+handle+"/tokens/:address", func(c *gin.Context) {
+		endpoint.GetTokensByAddress(c, tokenAPI)
+	})
+}
+
+func RegisterTransactionsAPI(root gin.IRouter, api blockatlas.Platform) {
+	txAPI, _ := api.(blockatlas.TxAPI)
+	tokenTxAPI, _ := api.(blockatlas.TokenTxAPI)
+
+	handle := api.Coin().Handle
+
+	if IsForCustomAPI(handle) {
+		return
+	}
+
+	root.GET("/v1/"+handle+"/:address", func(c *gin.Context) {
+		endpoint.GetTransactionsHistory(c, txAPI, tokenTxAPI)
+	})
+	root.GET("/v2/"+handle+"/transactions/:address", func(c *gin.Context) {
+		endpoint.GetTransactionsHistory(c, txAPI, tokenTxAPI)
+	})
+}
+
+func RegisterStakeAPI(root gin.IRouter, api blockatlas.Platform) {
+	stakingAPI, ok := api.(blockatlas.StakeAPI)
+	if !ok {
+		return
+	}
+	handle := api.Coin().Handle
+
+	root.GET("/v2/"+handle+"/staking/validators", func(c *gin.Context) {
+		endpoint.GetValidators(c, stakingAPI)
+	})
+	root.GET("/v2/"+handle+"/staking/delegations/:address", func(c *gin.Context) {
+		endpoint.GetStakingDelegationsForSpecificCoin(c, stakingAPI)
+	})
+}
+
+func RegisterBatchAPI(root gin.IRouter) {
+	root.POST("v2/staking/delegations", func(c *gin.Context) {
+		endpoint.GetStakeDelegationsWithAllInfoForBatch(c, platform.StakeAPIs)
+	})
+	root.POST("v2/staking/list", func(c *gin.Context) {
+		endpoint.GetStakeInfoForBatch(c, platform.StakeAPIs)
+	})
+	root.POST("/v3/collectibles/categories", func(c *gin.Context) {
+		endpoint.GetCollectionCategoriesFromListV3(c, platform.CollectionAPIs)
+	})
+	root.POST("/v4/collectibles/categories", func(c *gin.Context) {
+		endpoint.GetCollectionCategoriesFromList(c, platform.CollectionAPIs)
+	})
+}
+
+// CustomAPI must be removed and all handlers needs to be migrated to the transactions, tokens api
+func RegisterCustomAPI(root gin.IRouter, api blockatlas.Platform) {
+	customAPI, ok := api.(blockatlas.CustomAPI)
+	if !ok {
+		return
+	}
+	handle := api.Coin().Handle
+
+	customRouter := root.Group("/v1/" + handle)
+	customAPI.RegisterRoutes(customRouter)
+}
+
+func RegisterDomainAPI(root gin.IRouter) {
+	root.GET("/ns/lookup", endpoint.GetAddressByCoinAndDomain)
+	root.GET("v2/ns/lookup", endpoint.GetAddressByCoinAndDomainBatch)
+}
+
+func RegisterBasicAPI(root gin.IRouter) {
+	root.GET("/", endpoint.GetRoot)
+	root.GET("/status", endpoint.GetStatus)
+}
+
+func IsForCustomAPI(handle string) bool {
+	switch handle {
+	case coin.Gochain().Handle,
+		coin.Thundertoken().Handle,
+		coin.Classic().Handle,
+		coin.Poa().Handle,
+		coin.Callisto().Handle,
+		coin.Wanchain().Handle,
+		coin.Tomochain().Handle,
+		coin.Ethereum().Handle,
+		coin.Bitcoin().Handle,
+		coin.Litecoin().Handle,
+		coin.Bitcoincash().Handle,
+		coin.Zcash().Handle,
+		coin.Zcoin().Handle,
+		coin.Viacoin().Handle,
+		coin.Ravencoin().Handle,
+		coin.Groestlcoin().Handle,
+		coin.Zelcash().Handle,
+		coin.Decred().Handle,
+		coin.Digibyte().Handle,
+		coin.Dash().Handle,
+		coin.Doge().Handle,
+		coin.Qtum().Handle:
+		return true
+	default:
+		return false
+	}
+}
diff --git a/api/routes.go b/api/routes.go
deleted file mode 100644
index c843a21..0000000
--- a/api/routes.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package api
-
-import (
-	"fmt"
-	"github.com/gin-gonic/gin"
-	"github.com/trustwallet/blockatlas/internal"
-	"github.com/trustwallet/blockatlas/pkg/ginutils"
-	"github.com/trustwallet/blockatlas/pkg/logger"
-	"github.com/trustwallet/blockatlas/platform"
-)
-
-var routers = make(map[string]gin.IRouter)
-
-func SetupPlatformAPI(root gin.IRouter) {
-	root.GET("/", GetRoot)
-	root.GET("/status", GetStatus)
-
-	v1 := root.Group("/v1")
-	v2 := root.Group("/v2")
-	v3 := root.Group("/v3")
-	v4 := root.Group("/v4")
-
-	v1.GET("/", GetSupportedEndpoints)
-
-	for _, txAPI := range platform.Platforms {
-		router := getRouter(v1, txAPI.Coin().Handle)
-		makeTxRouteV1(router, txAPI)
-
-		routerV2 := getRouter(v2, txAPI.Coin().Handle)
-		makeTxRouteV2(routerV2, txAPI)
-	}
-
-	for _, tokenAPI := range platform.Platforms {
-		router := getRouter(v2, tokenAPI.Coin().Handle)
-		makeTokenRoute(router, tokenAPI)
-	}
-
-	for _, stakeAPI := range platform.Platforms {
-		router := getRouter(v2, stakeAPI.Coin().Handle)
-		makeStakingValidatorsRoute(router, stakeAPI)
-		makeStakingDelegationsRoute(router, stakeAPI)
-	}
-
-	for _, collectionAPI := range platform.Platforms {
-		routerV2 := getRouter(v2, collectionAPI.Coin().Handle)
-		routerV3 := getRouter(v3, collectionAPI.Coin().Handle)
-		routerV4 := getRouter(v4, collectionAPI.Coin().Handle)
-
-		makeCollectionsRoute(routerV3, collectionAPI)
-		makeCollectionRoute(routerV3, collectionAPI)
-
-		oldMakeCollectionRoute(routerV2, collectionAPI)
-		oldMakeCollectionsRoute(routerV2, collectionAPI)
-
-		makeCollectionRouteV4(routerV4, collectionAPI)
-	}
-
-	for _, customAPI := range platform.CustomAPIs {
-		router := getRouter(v1, customAPI.Coin().Handle)
-		customAPI.RegisterRoutes(router)
-	}
-
-	ns := root.Group("/ns")
-	batchNs := v2.Group("/ns")
-	MakeLookupRoute(ns)
-	MakeLookupBatchRoute(batchNs)
-
-	oldMakeCategoriesBatchRoute(v2)
-	makeCategoriesBatchRoute(v3)
-	makeCategoriesBatchRouteV4(v4)
-	makeStakingDelegationsBatchRoute(v2)
-	makeStakingDelegationsSimpleBatchRoute(v2)
-
-	logger.Info("Routes set up", logger.Params{"routes": len(routers)})
-}
-
-// getRouter lazy loads routers
-func getRouter(router *gin.RouterGroup, handle string) gin.IRouter {
-	key := fmt.Sprintf("%s/%s", router.BasePath(), handle)
-	if group, ok := routers[key]; ok {
-		return group
-	} else {
-		path := fmt.Sprintf("/%s", handle)
-		logger.Debug("Registering route", logger.Params{"path": len(path)})
-		group := router.Group(path)
-		routers[key] = group
-		return group
-	}
-}
-
-func GetSupportedEndpoints(c *gin.Context) {
-	var resp struct {
-		Endpoints []string `json:"endpoints,omitempty"`
-	}
-	for handle := range routers {
-		resp.Endpoints = append(resp.Endpoints, handle)
-	}
-	ginutils.RenderSuccess(c, &resp)
-}
-
-func GetRoot(c *gin.Context) { ginutils.RenderSuccess(c, "Welcome to the Block Atlas API") }
-
-func GetStatus(c *gin.Context) {
-	ginutils.RenderSuccess(c, map[string]interface{}{
-		"status": true,
-		"build":  internal.Build,
-		"date":   internal.Date,
-	})
-}
diff --git a/api/staking.go b/api/staking.go
deleted file mode 100644
index 881d26b..0000000
--- a/api/staking.go
+++ /dev/null
@@ -1,192 +0,0 @@
-package api
-
-import (
-	"github.com/gin-gonic/gin"
-	"github.com/trustwallet/blockatlas/coin"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"github.com/trustwallet/blockatlas/pkg/errors"
-	"github.com/trustwallet/blockatlas/pkg/ginutils"
-	"github.com/trustwallet/blockatlas/pkg/ginutils/gincache"
-	"github.com/trustwallet/blockatlas/pkg/logger"
-	"github.com/trustwallet/blockatlas/platform"
-	services "github.com/trustwallet/blockatlas/services/assets"
-	"time"
-)
-
-type (
-	AddressBatchRequest struct {
-		Address string `json:"address"`
-		CoinBatchRequest
-	}
-
-	CoinBatchRequest struct {
-		Coin uint `json:"coin"`
-	}
-
-	ENSBatchRequest struct {
-		Coins []uint64 `json:"coins"`
-		Name  string   `json:"name"`
-	}
-
-	AddressesRequest []AddressBatchRequest
-	CoinsRequest     []CoinBatchRequest
-)
-
-// @Summary Get Multiple Stake Delegations
-// @ID batch_delegations
-// @Description Get Stake Delegations for multiple coins
-// @Accept json
-// @Produce json
-// @Tags Staking
-// @Param delegations body api.AddressesRequest true "Validators addresses and coins"
-// @Success 200 {object} blockatlas.DelegationsBatchPage
-// @Router /v2/staking/delegations [post]
-func makeStakingDelegationsBatchRoute(router gin.IRouter) {
-	router.POST("/staking/delegations", func(c *gin.Context) {
-		var reqs AddressesRequest
-		if err := c.BindJSON(&reqs); err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		batch := make(blockatlas.DelegationsBatchPage, 0)
-		for _, r := range reqs {
-			c, ok := coin.Coins[r.Coin]
-			if !ok {
-				continue
-			}
-			p, ok := platform.StakeAPIs[c.Handle]
-			if !ok {
-				continue
-			}
-			delegation, err := getDelegationResponse(p, r.Address)
-			if err != nil {
-				continue
-			}
-			batch = append(batch, delegation)
-		}
-		ginutils.RenderSuccess(c, blockatlas.DocsResponse{Docs: batch})
-	})
-}
-
-// @Summary Get Multiple Stake Delegations
-// @ID batch_delegations
-// @Description Get Stake Delegations for multiple coins
-// @Accept json
-// @Produce json
-// @Tags Staking
-// @Param delegations body api.AddressesRequest true "Validators addresses and coins"
-// @Success 200 {object} blockatlas.DelegationsBatchPage
-// @Router /v2/staking/list [post]
-func makeStakingDelegationsSimpleBatchRoute(router gin.IRouter) {
-	router.POST("/staking/list", gincache.CacheMiddleware(time.Hour*24, func(c *gin.Context) {
-		var reqs CoinsRequest
-		if err := c.BindJSON(&reqs); err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		batch := make(blockatlas.StakingBatchPage, 0)
-		for _, r := range reqs {
-			c, ok := coin.Coins[r.Coin]
-			if !ok {
-				continue
-			}
-			p, ok := platform.StakeAPIs[c.Handle]
-			if !ok {
-				continue
-			}
-			staking := getStakingResponse(p)
-			batch = append(batch, staking)
-		}
-		ginutils.RenderSuccess(c, blockatlas.DocsResponse{Docs: batch})
-	}))
-}
-
-// @Summary Get Validators
-// @ID validators
-// @Description Get validators from the address
-// @Accept json
-// @Produce json
-// @Tags Staking
-// @Param coin path string true "the coin name" default(cosmos)
-// @Success 200 {object} blockatlas.DocsResponse
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v2/{coin}/staking/validators [get]
-func makeStakingValidatorsRoute(router gin.IRouter, api blockatlas.Platform) {
-	var stakingAPI blockatlas.StakeAPI
-	stakingAPI, _ = api.(blockatlas.StakeAPI)
-
-	if stakingAPI == nil {
-		return
-	}
-
-	router.GET("/staking/validators", gincache.CacheMiddleware(time.Hour, func(c *gin.Context) {
-		results, err := services.GetActiveValidators(stakingAPI)
-		if err != nil {
-			logger.Error(err)
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-		ginutils.RenderSuccess(c, blockatlas.DocsResponse{Docs: results})
-	}))
-}
-
-// @Summary Get Stake Delegations
-// @ID delegations
-// @Description Get stake delegations from the address
-// @Accept json
-// @Produce json
-// @Tags Staking
-// @Param coin path string true "the coin name" default(tron)
-// @Param address path string true "the query address" default(TPJYCz8ppZNyvw7pTwmjajcx4Kk1MmEUhD)
-// @Success 200 {object} blockatlas.DelegationResponse
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v2/{coin}/staking/delegations/{address} [get]
-func makeStakingDelegationsRoute(router gin.IRouter, api blockatlas.Platform) {
-	var stakingAPI blockatlas.StakeAPI
-	stakingAPI, _ = api.(blockatlas.StakeAPI)
-
-	if stakingAPI == nil {
-		return
-	}
-
-	router.GET("/staking/delegations/:address", func(c *gin.Context) {
-		response, err := getDelegationResponse(stakingAPI, c.Param("address"))
-		if err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		ginutils.RenderSuccess(c, response)
-	})
-}
-
-func getDelegationResponse(p blockatlas.StakeAPI, address string) (blockatlas.DelegationResponse, error) {
-	delegations, err := p.GetDelegations(address)
-	if err != nil {
-		return blockatlas.DelegationResponse{
-			StakingResponse: getStakingResponse(p),
-		}, errors.E("Unable to fetch delegations list", err)
-	}
-	balance, err := p.UndelegatedBalance(address)
-	if err != nil {
-		return blockatlas.DelegationResponse{
-			StakingResponse: getStakingResponse(p),
-		}, errors.E("Unable to fetch undelegated balance", err)
-	}
-	return blockatlas.DelegationResponse{
-		Balance:         balance,
-		Delegations:     delegations,
-		Address:         address,
-		StakingResponse: getStakingResponse(p),
-	}, nil
-}
-
-func getStakingResponse(p blockatlas.StakeAPI) blockatlas.StakingResponse {
-	c := p.Coin()
-	return blockatlas.StakingResponse{
-		Coin:    c.External(),
-		Details: p.GetDetails(),
-	}
-}
diff --git a/api/transaction.go b/api/transaction.go
deleted file mode 100644
index 002943d..0000000
--- a/api/transaction.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package api
-
-import (
-	"github.com/gin-gonic/gin"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"github.com/trustwallet/blockatlas/pkg/ginutils"
-	"net/http"
-	"sort"
-)
-
-// @Summary Get Transactions
-// @ID tx_v1
-// @Description Get transactions from the address
-// @Accept json
-// @Produce json
-// @Tags Transactions
-// @Param coin path string true "the coin name" default(tezos)
-// @Param address path string true "the query address" default(tz1WCd2jm4uSt4vntk4vSuUWoZQGhLcDuR9q)
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v1/{coin}/{address} [get]
-func makeTxRouteV1(router gin.IRouter, api blockatlas.Platform) {
-	makeTxRoute(router, api, "/:address")
-}
-
-// @Summary Get Transactions
-// @ID tx_v2
-// @Description Get transactions from the address
-// @Accept json
-// @Produce json
-// @Tags Transactions
-// @Param coin path string true "the coin name" default(tezos)
-// @Param address path string true "the query address" default(tz1WCd2jm4uSt4vntk4vSuUWoZQGhLcDuR9q)
-// @Success 200 {object} blockatlas.TxPage
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v2/{coin}/transactions/{address} [get]
-func makeTxRouteV2(router gin.IRouter, api blockatlas.Platform) {
-	makeTxRoute(router, api, "/transactions/:address")
-}
-
-func makeTxRoute(router gin.IRouter, api blockatlas.Platform, path string) {
-	var txAPI blockatlas.TxAPI
-	var tokenTxAPI blockatlas.TokenTxAPI
-	txAPI, _ = api.(blockatlas.TxAPI)
-	tokenTxAPI, _ = api.(blockatlas.TokenTxAPI)
-
-	if txAPI == nil && tokenTxAPI == nil {
-		return
-	}
-
-	router.GET(path, func(c *gin.Context) {
-		address := c.Param("address")
-		if address == "" {
-			emptyPage(c)
-			return
-		}
-		token := c.Query("token")
-
-		var txs []blockatlas.Tx
-		var err error
-		switch {
-		case token == "" && txAPI != nil:
-			txs, err = txAPI.GetTxsByAddress(address)
-		case token != "" && tokenTxAPI != nil:
-			txs, err = tokenTxAPI.GetTokenTxsByAddress(address, token)
-		default:
-			emptyPage(c)
-			return
-		}
-
-		if err != nil {
-			errResp := ginutils.ErrorResponse(c)
-			switch {
-			case err == blockatlas.ErrInvalidAddr:
-				errResp.Params(http.StatusBadRequest, "Invalid address")
-			case err == blockatlas.ErrNotFound:
-				errResp.Params(http.StatusNotFound, "No such address")
-			case err == blockatlas.ErrSourceConn:
-				errResp.Params(http.StatusServiceUnavailable, "Lost connection to blockchain")
-			}
-			errResp.Render()
-			return
-		}
-
-		page := make(blockatlas.TxPage, 0)
-		for _, tx := range txs {
-			if tx.Direction != "" {
-				goto AddTx
-			}
-			tx.Direction = blockatlas.DirectionOutgoing
-			if tx.To == address {
-				tx.Direction = blockatlas.DirectionIncoming
-				if tx.From == address {
-					tx.Direction = blockatlas.DirectionSelf
-				}
-			}
-		AddTx:
-			page = append(page, tx)
-		}
-		if len(page) > blockatlas.TxPerPage {
-			page = page[0:blockatlas.TxPerPage]
-		}
-		sort.Sort(page)
-		ginutils.RenderSuccess(c, &page)
-	})
-}
-
-// @Summary Get Tokens
-// @ID tokens
-// @Description Get tokens from the address
-// @Accept json
-// @Produce json
-// @Tags Transactions
-// @Param coin path string true "the coin name" default(ethereum)
-// @Param address path string true "the query address" default(0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB)
-// @Success 200 {object} blockatlas.CollectionPage
-// @Failure 500 {object} ginutils.ApiError
-// @Router /v2/{coin}/tokens/{address} [get]
-func makeTokenRoute(router gin.IRouter, api blockatlas.Platform) {
-	var tokenAPI blockatlas.TokenAPI
-	tokenAPI, _ = api.(blockatlas.TokenAPI)
-
-	if tokenAPI == nil {
-		return
-	}
-
-	router.GET("/tokens/:address", func(c *gin.Context) {
-		address := c.Param("address")
-		if address == "" {
-			emptyPage(c)
-			return
-		}
-
-		tl, err := tokenAPI.GetTokenListByAddress(address)
-		if err != nil {
-			ginutils.ErrorResponse(c).Message(err.Error()).Render()
-			return
-		}
-
-		ginutils.RenderSuccess(c, blockatlas.DocsResponse{Docs: tl})
-	})
-}
diff --git a/docs/docs.go b/docs/docs.go
index 247a273..99f6503 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -1,6 +1,6 @@
 // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
 // This file was generated by swaggo/swag at
-// 2020-03-01 04:39:26.356879 +0300 MSK m=+1.896562991
+// 2020-04-06 16:53:26.62462 +0300 MSK m=+1.619745776
 
 package docs
 
@@ -157,13 +157,6 @@ var doc = `{
                         "name": "token",
                         "in": "query"
                     },
-                    {
-                        "type": "integer",
-                        "default": 1574483028,
-                        "description": "Start timestamp",
-                        "name": "time_start",
-                        "in": "query"
-                    },
                     {
                         "type": "string",
                         "default": "USD",
@@ -301,42 +294,6 @@ var doc = `{
                 }
             }
         },
-        "/v2/collectibles/categories": {
-            "post": {
-                "description": "Get collection categories",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get list of collections from a specific coin and addresses",
-                "operationId": "collection_categories_v2",
-                "parameters": [
-                    {
-                        "default": "{\"60\": [\"0xb3624367b1ab37daef42e1a3a2ced012359659b0\"]}",
-                        "description": "Payload",
-                        "name": "data",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "type": "string"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.DocsResponse"
-                        }
-                    }
-                }
-            }
-        },
         "/v2/ns/lookup": {
             "get": {
                 "description": "Lookup ENS/ZNS to find registered addresses for multiple coins",
@@ -452,110 +409,6 @@ var doc = `{
                 }
             }
         },
-        "/v2/{coin}/collections/{address}": {
-            "get": {
-                "description": "Get all collections from the address",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get Collections",
-                "operationId": "collections_v2",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "default": "ethereum",
-                        "description": "the coin name",
-                        "name": "coin",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-                        "description": "the query address",
-                        "name": "address",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.CollectionPage"
-                        }
-                    },
-                    "500": {
-                        "description": "Internal Server Error",
-                        "schema": {
-                            "$ref": "#/definitions/ginutils.ApiError"
-                        }
-                    }
-                }
-            }
-        },
-        "/v2/{coin}/collections/{owner}/collection/{collection_id}": {
-            "get": {
-                "description": "Get a collection from the address",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get Collection",
-                "operationId": "collection_v2",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "default": "ethereum",
-                        "description": "the coin name",
-                        "name": "coin",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
-                        "description": "the query address",
-                        "name": "owner",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x06012c8cf97bead5deae237070f9587f8e7a266d",
-                        "description": "the query collection",
-                        "name": "collection_id",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.CollectionPage"
-                        }
-                    },
-                    "500": {
-                        "description": "Internal Server Error",
-                        "schema": {
-                            "$ref": "#/definitions/ginutils.ApiError"
-                        }
-                    }
-                }
-            }
-        },
         "/v2/{coin}/staking/delegations/{address}": {
             "get": {
                 "description": "Get stake delegations from the address",
@@ -740,146 +593,6 @@ var doc = `{
                 }
             }
         },
-        "/v3/collectibles/categories": {
-            "post": {
-                "description": "Get collection categories",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get list of collections from a specific coin and addresses",
-                "operationId": "collection_categories_v3",
-                "parameters": [
-                    {
-                        "default": "{\"60\": [\"0xb3624367b1ab37daef42e1a3a2ced012359659b0\"]}",
-                        "description": "Payload",
-                        "name": "data",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "type": "string"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.DocsResponse"
-                        }
-                    }
-                }
-            }
-        },
-        "/v3/{coin}/collections/{address}": {
-            "get": {
-                "description": "Get all collections from the address",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get Collections",
-                "operationId": "collections_v3",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "default": "ethereum",
-                        "description": "the coin name",
-                        "name": "coin",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-                        "description": "the query address",
-                        "name": "address",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.CollectionPage"
-                        }
-                    },
-                    "500": {
-                        "description": "Internal Server Error",
-                        "schema": {
-                            "$ref": "#/definitions/ginutils.ApiError"
-                        }
-                    }
-                }
-            }
-        },
-        "/v3/{coin}/collections/{owner}/collection/{collection_id}": {
-            "get": {
-                "description": "Get a collection from the address",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get Collection",
-                "operationId": "collection_v3",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "default": "ethereum",
-                        "description": "the coin name",
-                        "name": "coin",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
-                        "description": "the query address",
-                        "name": "owner",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x06012c8cf97bead5deae237070f9587f8e7a266d",
-                        "description": "the query collection",
-                        "name": "collection_id",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.CollectionPage"
-                        }
-                    },
-                    "500": {
-                        "description": "Internal Server Error",
-                        "schema": {
-                            "$ref": "#/definitions/ginutils.ApiError"
-                        }
-                    }
-                }
-            }
-        },
         "/v4/collectibles/categories": {
             "post": {
                 "description": "Get collection categories",
@@ -1001,7 +714,8 @@ var doc = `{
                     "type": "string"
                 },
                 "type": {
-                    "type": "string"
+                    "type": "object",
+                    "$ref": "#/definitions/blockatlas.CoinStatus"
                 }
             }
         },
@@ -1069,6 +783,17 @@ var doc = `{
                 "$ref": "#/definitions/api.Ticker"
             }
         },
+        "blockatlas.CoinStatus": {
+            "type": "object",
+            "properties": {
+                "error": {
+                    "type": "string"
+                },
+                "height": {
+                    "type": "integer"
+                }
+            }
+        },
         "blockatlas.Collection": {
             "type": "object",
             "properties": {
diff --git a/docs/swagger.json b/docs/swagger.json
index dc8104c..52096ec 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -136,13 +136,6 @@
                         "name": "token",
                         "in": "query"
                     },
-                    {
-                        "type": "integer",
-                        "default": 1574483028,
-                        "description": "Start timestamp",
-                        "name": "time_start",
-                        "in": "query"
-                    },
                     {
                         "type": "string",
                         "default": "USD",
@@ -280,42 +273,6 @@
                 }
             }
         },
-        "/v2/collectibles/categories": {
-            "post": {
-                "description": "Get collection categories",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get list of collections from a specific coin and addresses",
-                "operationId": "collection_categories_v2",
-                "parameters": [
-                    {
-                        "default": "{\"60\": [\"0xb3624367b1ab37daef42e1a3a2ced012359659b0\"]}",
-                        "description": "Payload",
-                        "name": "data",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "type": "string"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.DocsResponse"
-                        }
-                    }
-                }
-            }
-        },
         "/v2/ns/lookup": {
             "get": {
                 "description": "Lookup ENS/ZNS to find registered addresses for multiple coins",
@@ -431,110 +388,6 @@
                 }
             }
         },
-        "/v2/{coin}/collections/{address}": {
-            "get": {
-                "description": "Get all collections from the address",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get Collections",
-                "operationId": "collections_v2",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "default": "ethereum",
-                        "description": "the coin name",
-                        "name": "coin",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-                        "description": "the query address",
-                        "name": "address",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.CollectionPage"
-                        }
-                    },
-                    "500": {
-                        "description": "Internal Server Error",
-                        "schema": {
-                            "$ref": "#/definitions/ginutils.ApiError"
-                        }
-                    }
-                }
-            }
-        },
-        "/v2/{coin}/collections/{owner}/collection/{collection_id}": {
-            "get": {
-                "description": "Get a collection from the address",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get Collection",
-                "operationId": "collection_v2",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "default": "ethereum",
-                        "description": "the coin name",
-                        "name": "coin",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
-                        "description": "the query address",
-                        "name": "owner",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x06012c8cf97bead5deae237070f9587f8e7a266d",
-                        "description": "the query collection",
-                        "name": "collection_id",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.CollectionPage"
-                        }
-                    },
-                    "500": {
-                        "description": "Internal Server Error",
-                        "schema": {
-                            "$ref": "#/definitions/ginutils.ApiError"
-                        }
-                    }
-                }
-            }
-        },
         "/v2/{coin}/staking/delegations/{address}": {
             "get": {
                 "description": "Get stake delegations from the address",
@@ -719,146 +572,6 @@
                 }
             }
         },
-        "/v3/collectibles/categories": {
-            "post": {
-                "description": "Get collection categories",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get list of collections from a specific coin and addresses",
-                "operationId": "collection_categories_v3",
-                "parameters": [
-                    {
-                        "default": "{\"60\": [\"0xb3624367b1ab37daef42e1a3a2ced012359659b0\"]}",
-                        "description": "Payload",
-                        "name": "data",
-                        "in": "body",
-                        "required": true,
-                        "schema": {
-                            "type": "string"
-                        }
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.DocsResponse"
-                        }
-                    }
-                }
-            }
-        },
-        "/v3/{coin}/collections/{address}": {
-            "get": {
-                "description": "Get all collections from the address",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get Collections",
-                "operationId": "collections_v3",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "default": "ethereum",
-                        "description": "the coin name",
-                        "name": "coin",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-                        "description": "the query address",
-                        "name": "address",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.CollectionPage"
-                        }
-                    },
-                    "500": {
-                        "description": "Internal Server Error",
-                        "schema": {
-                            "$ref": "#/definitions/ginutils.ApiError"
-                        }
-                    }
-                }
-            }
-        },
-        "/v3/{coin}/collections/{owner}/collection/{collection_id}": {
-            "get": {
-                "description": "Get a collection from the address",
-                "consumes": [
-                    "application/json"
-                ],
-                "produces": [
-                    "application/json"
-                ],
-                "tags": [
-                    "Collections"
-                ],
-                "summary": "Get Collection",
-                "operationId": "collection_v3",
-                "parameters": [
-                    {
-                        "type": "string",
-                        "default": "ethereum",
-                        "description": "the coin name",
-                        "name": "coin",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
-                        "description": "the query address",
-                        "name": "owner",
-                        "in": "path",
-                        "required": true
-                    },
-                    {
-                        "type": "string",
-                        "default": "0x06012c8cf97bead5deae237070f9587f8e7a266d",
-                        "description": "the query collection",
-                        "name": "collection_id",
-                        "in": "path",
-                        "required": true
-                    }
-                ],
-                "responses": {
-                    "200": {
-                        "description": "OK",
-                        "schema": {
-                            "$ref": "#/definitions/blockatlas.CollectionPage"
-                        }
-                    },
-                    "500": {
-                        "description": "Internal Server Error",
-                        "schema": {
-                            "$ref": "#/definitions/ginutils.ApiError"
-                        }
-                    }
-                }
-            }
-        },
         "/v4/collectibles/categories": {
             "post": {
                 "description": "Get collection categories",
@@ -980,7 +693,8 @@
                     "type": "string"
                 },
                 "type": {
-                    "type": "string"
+                    "type": "object",
+                    "$ref": "#/definitions/blockatlas.CoinStatus"
                 }
             }
         },
@@ -1048,6 +762,17 @@
                 "$ref": "#/definitions/api.Ticker"
             }
         },
+        "blockatlas.CoinStatus": {
+            "type": "object",
+            "properties": {
+                "error": {
+                    "type": "string"
+                },
+                "height": {
+                    "type": "integer"
+                }
+            }
+        },
         "blockatlas.Collection": {
             "type": "object",
             "properties": {
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index 4ddb42c..d91e4c5 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -17,7 +17,8 @@ definitions:
       token_id:
         type: string
       type:
-        type: string
+        $ref: '#/definitions/blockatlas.CoinStatus'
+        type: object
     type: object
   api.Ticker:
     properties:
@@ -61,6 +62,13 @@ definitions:
     items:
       $ref: '#/definitions/api.Ticker'
     type: array
+  blockatlas.CoinStatus:
+    properties:
+      error:
+        type: string
+      height:
+        type: integer
+    type: object
   blockatlas.Collection:
     properties:
       address:
@@ -300,16 +308,16 @@ paths:
       description: Lookup ENS/ZNS to find registered addresses
       operationId: lookup
       parameters:
-      - description: string name
-        in: query
-        name: name
-        type: string
-      - description: string coin
-        in: query
-        name: coin
-        type: string
+        - description: string name
+          in: query
+          name: name
+          type: string
+        - description: string coin
+          in: query
+          name: coin
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -321,28 +329,28 @@ paths:
             $ref: '#/definitions/ginutils.ApiError'
       summary: Lookup .eth / .zil addresses
       tags:
-      - Naming
+        - Naming
   /v1/{coin}/{address}:
     get:
       consumes:
-      - application/json
+        - application/json
       description: Get transactions from the address
       operationId: tx_v1
       parameters:
-      - default: tezos
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
-      - default: tz1WCd2jm4uSt4vntk4vSuUWoZQGhLcDuR9q
-        description: the query address
-        in: path
-        name: address
-        required: true
-        type: string
+        - default: tezos
+          description: the coin name
+          in: path
+          name: coin
+          required: true
+          type: string
+        - default: tz1WCd2jm4uSt4vntk4vSuUWoZQGhLcDuR9q
+          description: the query address
+          in: path
+          name: address
+          required: true
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "500":
           description: Internal Server Error
@@ -350,28 +358,28 @@ paths:
             $ref: '#/definitions/ginutils.ApiError'
       summary: Get Transactions
       tags:
-      - Transactions
+        - Transactions
   /v1/{coin}/xpub/{xpub}:
     get:
       consumes:
-      - application/json
+        - application/json
       description: Get transactions from xpub address
       operationId: xpub
       parameters:
-      - default: bitcoin
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
-      - default: zpub6ruK9k6YGm8BRHWvTiQcrEPnFkuRDJhR7mPYzV2LDvjpLa5CuGgrhCYVZjMGcLcFqv9b2WvsFtY2Gb3xq8NVq8qhk9veozrA2W9QaWtihrC
-        description: the xpub address
-        in: path
-        name: xpub
-        required: true
-        type: string
+        - default: bitcoin
+          description: the coin name
+          in: path
+          name: coin
+          required: true
+          type: string
+        - default: zpub6ruK9k6YGm8BRHWvTiQcrEPnFkuRDJhR7mPYzV2LDvjpLa5CuGgrhCYVZjMGcLcFqv9b2WvsFtY2Gb3xq8NVq8qhk9veozrA2W9QaWtihrC
+          description: the xpub address
+          in: path
+          name: xpub
+          required: true
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -379,41 +387,41 @@ paths:
             $ref: '#/definitions/blockatlas.TxPage'
       summary: Get xpub transactions
       tags:
-      - Transactions
+        - Transactions
   /v1/market/charts:
     get:
       consumes:
-      - application/json
+        - application/json
       description: Get the charts data from an market and coin/token
       operationId: get_charts_data
       parameters:
-      - default: 60
-        description: Coin ID
-        in: query
-        name: coin
-        required: true
-        type: integer
-      - description: Token ID
-        in: query
-        name: token
-        type: string
-      - default: 1574483028
-        description: Start timestamp
-        in: query
-        name: time_start
-        type: integer
-      - default: 64
-        description: Max number of items in result prices array
-        in: query
-        name: max_items
-        type: integer
-      - default: USD
-        description: The currency to show charts
-        in: query
-        name: currency
-        type: string
+        - default: 60
+          description: Coin ID
+          in: query
+          name: coin
+          required: true
+          type: integer
+        - description: Token ID
+          in: query
+          name: token
+          type: string
+        - default: 1574483028
+          description: Start timestamp
+          in: query
+          name: time_start
+          type: integer
+        - default: 64
+          description: Max number of items in result prices array
+          in: query
+          name: max_items
+          type: integer
+        - default: USD
+          description: The currency to show charts
+          in: query
+          name: currency
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -421,36 +429,31 @@ paths:
             $ref: '#/definitions/watchmarket.ChartData'
       summary: Get charts data for a specific coin
       tags:
-      - Market
+        - Market
   /v1/market/info:
     get:
       consumes:
-      - application/json
+        - application/json
       description: Get the charts coin info data from an market and coin/contract
       operationId: get_charts_coin_info
       parameters:
-      - default: 60
-        description: Coin ID
-        in: query
-        name: coin
-        required: true
-        type: integer
-      - description: Token ID
-        in: query
-        name: token
-        type: string
-      - default: 1574483028
-        description: Start timestamp
-        in: query
-        name: time_start
-        type: integer
-      - default: USD
-        description: The currency to show coin info in
-        in: query
-        name: currency
-        type: string
+        - default: 60
+          description: Coin ID
+          in: query
+          name: coin
+          required: true
+          type: integer
+        - description: Token ID
+          in: query
+          name: token
+          type: string
+        - default: USD
+          description: The currency to show coin info in
+          in: query
+          name: currency
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -458,22 +461,22 @@ paths:
             $ref: '#/definitions/watchmarket.ChartCoinInfo'
       summary: Get charts coin info data for a specific coin
       tags:
-      - Market
+        - Market
   /v1/market/ticker:
     post:
       consumes:
-      - application/json
+        - application/json
       description: Get the ticker values from many market and coin/token
       operationId: get_tickers
       parameters:
-      - description: Ticker
-        in: body
-        name: tickers
-        required: true
-        schema:
-          $ref: '#/definitions/api.TickerRequest'
+        - description: Ticker
+          in: body
+          name: tickers
+          required: true
+          schema:
+            $ref: '#/definitions/api.TickerRequest'
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -481,100 +484,28 @@ paths:
             $ref: '#/definitions/api.Tickers'
       summary: Get ticker values for a specific market
       tags:
-      - Market
-  /v2/{coin}/collections/{address}:
-    get:
-      consumes:
-      - application/json
-      description: Get all collections from the address
-      operationId: collections_v2
-      parameters:
-      - default: ethereum
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
-      - default: 0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB
-        description: the query address
-        in: path
-        name: address
-        required: true
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            $ref: '#/definitions/blockatlas.CollectionPage'
-        "500":
-          description: Internal Server Error
-          schema:
-            $ref: '#/definitions/ginutils.ApiError'
-      summary: Get Collections
-      tags:
-      - Collections
-  /v2/{coin}/collections/{owner}/collection/{collection_id}:
-    get:
-      consumes:
-      - application/json
-      description: Get a collection from the address
-      operationId: collection_v2
-      parameters:
-      - default: ethereum
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
-      - default: 0x0875BCab22dE3d02402bc38aEe4104e1239374a7
-        description: the query address
-        in: path
-        name: owner
-        required: true
-        type: string
-      - default: 0x06012c8cf97bead5deae237070f9587f8e7a266d
-        description: the query collection
-        in: path
-        name: collection_id
-        required: true
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            $ref: '#/definitions/blockatlas.CollectionPage'
-        "500":
-          description: Internal Server Error
-          schema:
-            $ref: '#/definitions/ginutils.ApiError'
-      summary: Get Collection
-      tags:
-      - Collections
+        - Market
   /v2/{coin}/staking/delegations/{address}:
     get:
       consumes:
-      - application/json
+        - application/json
       description: Get stake delegations from the address
       operationId: delegations
       parameters:
-      - default: tron
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
-      - default: TPJYCz8ppZNyvw7pTwmjajcx4Kk1MmEUhD
-        description: the query address
-        in: path
-        name: address
-        required: true
-        type: string
+        - default: tron
+          description: the coin name
+          in: path
+          name: coin
+          required: true
+          type: string
+        - default: TPJYCz8ppZNyvw7pTwmjajcx4Kk1MmEUhD
+          description: the query address
+          in: path
+          name: address
+          required: true
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -586,22 +517,22 @@ paths:
             $ref: '#/definitions/ginutils.ApiError'
       summary: Get Stake Delegations
       tags:
-      - Staking
+        - Staking
   /v2/{coin}/staking/validators:
     get:
       consumes:
-      - application/json
+        - application/json
       description: Get validators from the address
       operationId: validators
       parameters:
-      - default: cosmos
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
+        - default: cosmos
+          description: the coin name
+          in: path
+          name: coin
+          required: true
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -613,28 +544,28 @@ paths:
             $ref: '#/definitions/ginutils.ApiError'
       summary: Get Validators
       tags:
-      - Staking
+        - Staking
   /v2/{coin}/tokens/{address}:
     get:
       consumes:
-      - application/json
+        - application/json
       description: Get tokens from the address
       operationId: tokens
       parameters:
-      - default: ethereum
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
-      - default: 0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB
-        description: the query address
-        in: path
-        name: address
-        required: true
-        type: string
+        - default: ethereum
+          description: the coin name
+          in: path
+          name: coin
+          required: true
+          type: string
+        - default: 0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB
+          description: the query address
+          in: path
+          name: address
+          required: true
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -646,28 +577,28 @@ paths:
             $ref: '#/definitions/ginutils.ApiError'
       summary: Get Tokens
       tags:
-      - Transactions
+        - Transactions
   /v2/{coin}/transactions/{address}:
     get:
       consumes:
-      - application/json
+        - application/json
       description: Get transactions from the address
       operationId: tx_v2
       parameters:
-      - default: tezos
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
-      - default: tz1WCd2jm4uSt4vntk4vSuUWoZQGhLcDuR9q
-        description: the query address
-        in: path
-        name: address
-        required: true
-        type: string
+        - default: tezos
+          description: the coin name
+          in: path
+          name: coin
+          required: true
+          type: string
+        - default: tz1WCd2jm4uSt4vntk4vSuUWoZQGhLcDuR9q
+          description: the query address
+          in: path
+          name: address
+          required: true
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -679,47 +610,23 @@ paths:
             $ref: '#/definitions/ginutils.ApiError'
       summary: Get Transactions
       tags:
-      - Transactions
-  /v2/collectibles/categories:
-    post:
-      consumes:
-      - application/json
-      description: Get collection categories
-      operationId: collection_categories_v2
-      parameters:
-      - default: '{"60": ["0xb3624367b1ab37daef42e1a3a2ced012359659b0"]}'
-        description: Payload
-        in: body
-        name: data
-        required: true
-        schema:
-          type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            $ref: '#/definitions/blockatlas.DocsResponse'
-      summary: Get list of collections from a specific coin and addresses
-      tags:
-      - Collections
+        - Transactions
   /v2/ns/lookup:
     get:
       description: Lookup ENS/ZNS to find registered addresses for multiple coins
       operationId: lookup
       parameters:
-      - description: string name
-        in: query
-        name: name
-        type: string
-      - description: List of coins
-        in: query
-        name: coins
-        required: true
-        type: string
+        - description: string name
+          in: query
+          name: name
+          type: string
+        - description: List of coins
+          in: query
+          name: coins
+          required: true
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -733,22 +640,22 @@ paths:
             $ref: '#/definitions/ginutils.ApiError'
       summary: Lookup .eth / .zil addresses
       tags:
-      - Naming
+        - Naming
   /v2/staking/delegations:
     post:
       consumes:
-      - application/json
+        - application/json
       description: Get Stake Delegations for multiple coins
       operationId: batch_delegations
       parameters:
-      - description: Validators addresses and coins
-        in: body
-        name: delegations
-        required: true
-        schema:
-          $ref: '#/definitions/api.AddressesRequest'
+        - description: Validators addresses and coins
+          in: body
+          name: delegations
+          required: true
+          schema:
+            $ref: '#/definitions/api.AddressesRequest'
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -756,22 +663,22 @@ paths:
             $ref: '#/definitions/blockatlas.DelegationsBatchPage'
       summary: Get Multiple Stake Delegations
       tags:
-      - Staking
+        - Staking
   /v2/staking/list:
     post:
       consumes:
-      - application/json
+        - application/json
       description: Get Stake Delegations for multiple coins
       operationId: batch_delegations
       parameters:
-      - description: Validators addresses and coins
-        in: body
-        name: delegations
-        required: true
-        schema:
-          $ref: '#/definitions/api.AddressesRequest'
+        - description: Validators addresses and coins
+          in: body
+          name: delegations
+          required: true
+          schema:
+            $ref: '#/definitions/api.AddressesRequest'
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -779,130 +686,34 @@ paths:
             $ref: '#/definitions/blockatlas.DelegationsBatchPage'
       summary: Get Multiple Stake Delegations
       tags:
-      - Staking
-  /v3/{coin}/collections/{address}:
-    get:
-      consumes:
-      - application/json
-      description: Get all collections from the address
-      operationId: collections_v3
-      parameters:
-      - default: ethereum
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
-      - default: 0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB
-        description: the query address
-        in: path
-        name: address
-        required: true
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            $ref: '#/definitions/blockatlas.CollectionPage'
-        "500":
-          description: Internal Server Error
-          schema:
-            $ref: '#/definitions/ginutils.ApiError'
-      summary: Get Collections
-      tags:
-      - Collections
-  /v3/{coin}/collections/{owner}/collection/{collection_id}:
-    get:
-      consumes:
-      - application/json
-      description: Get a collection from the address
-      operationId: collection_v3
-      parameters:
-      - default: ethereum
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
-      - default: 0x0875BCab22dE3d02402bc38aEe4104e1239374a7
-        description: the query address
-        in: path
-        name: owner
-        required: true
-        type: string
-      - default: 0x06012c8cf97bead5deae237070f9587f8e7a266d
-        description: the query collection
-        in: path
-        name: collection_id
-        required: true
-        type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            $ref: '#/definitions/blockatlas.CollectionPage'
-        "500":
-          description: Internal Server Error
-          schema:
-            $ref: '#/definitions/ginutils.ApiError'
-      summary: Get Collection
-      tags:
-      - Collections
-  /v3/collectibles/categories:
-    post:
-      consumes:
-      - application/json
-      description: Get collection categories
-      operationId: collection_categories_v3
-      parameters:
-      - default: '{"60": ["0xb3624367b1ab37daef42e1a3a2ced012359659b0"]}'
-        description: Payload
-        in: body
-        name: data
-        required: true
-        schema:
-          type: string
-      produces:
-      - application/json
-      responses:
-        "200":
-          description: OK
-          schema:
-            $ref: '#/definitions/blockatlas.DocsResponse'
-      summary: Get list of collections from a specific coin and addresses
-      tags:
-      - Collections
+        - Staking
   /v4/{coin}/collections/{owner}/collection/{collection_id}:
     get:
       consumes:
-      - application/json
+        - application/json
       description: Get a collection from the address
       operationId: collection_v4
       parameters:
-      - default: ethereum
-        description: the coin name
-        in: path
-        name: coin
-        required: true
-        type: string
-      - default: 0x0875BCab22dE3d02402bc38aEe4104e1239374a7
-        description: the query address
-        in: path
-        name: owner
-        required: true
-        type: string
-      - default: 0x06012c8cf97bead5deae237070f9587f8e7a266d
-        description: the query collection
-        in: path
-        name: collection_id
-        required: true
-        type: string
+        - default: ethereum
+          description: the coin name
+          in: path
+          name: coin
+          required: true
+          type: string
+        - default: 0x0875BCab22dE3d02402bc38aEe4104e1239374a7
+          description: the query address
+          in: path
+          name: owner
+          required: true
+          type: string
+        - default: 0x06012c8cf97bead5deae237070f9587f8e7a266d
+          description: the query collection
+          in: path
+          name: collection_id
+          required: true
+          type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -914,23 +725,23 @@ paths:
             $ref: '#/definitions/ginutils.ApiError'
       summary: Get Collection
       tags:
-      - Collections
+        - Collections
   /v4/collectibles/categories:
     post:
       consumes:
-      - application/json
+        - application/json
       description: Get collection categories
       operationId: collection_categories_v4
       parameters:
-      - default: '{"60": ["0xb3624367b1ab37daef42e1a3a2ced012359659b0"]}'
-        description: Payload
-        in: body
-        name: data
-        required: true
-        schema:
-          type: string
+        - default: '{"60": ["0xb3624367b1ab37daef42e1a3a2ced012359659b0"]}'
+          description: Payload
+          in: body
+          name: data
+          required: true
+          schema:
+            type: string
       produces:
-      - application/json
+        - application/json
       responses:
         "200":
           description: OK
@@ -938,5 +749,5 @@ paths:
             $ref: '#/definitions/blockatlas.DocsResponse'
       summary: Get list of collections from a specific coin and addresses
       tags:
-      - Collections
+        - Collections
 swagger: "2.0"
diff --git a/internal/init.go b/internal/init.go
index bce4270..5011df1 100644
--- a/internal/init.go
+++ b/internal/init.go
@@ -4,9 +4,9 @@ import (
 	"flag"
 	"fmt"
 	"github.com/gin-gonic/gin"
+	"github.com/trustwallet/blockatlas/api/middleware"
 	"github.com/trustwallet/blockatlas/config"
 	"github.com/trustwallet/blockatlas/mq"
-	"github.com/trustwallet/blockatlas/pkg/ginutils"
 	"github.com/trustwallet/blockatlas/pkg/logger"
 
 	"path/filepath"
@@ -43,11 +43,11 @@ func InitConfig(confPath string) {
 func InitEngine(handler *gin.HandlerFunc, ginMode string) *gin.Engine {
 	gin.SetMode(ginMode)
 	engine := gin.New()
-	engine.Use(ginutils.CheckReverseProxy, *handler)
-	engine.Use(ginutils.CORSMiddleware())
+	engine.Use(middleware.CheckReverseProxy, *handler)
+	engine.Use(middleware.CORSMiddleware())
 	engine.Use(gin.Logger())
 
-	engine.OPTIONS("/*path", ginutils.CORSMiddleware())
+	engine.OPTIONS("/*path", middleware.CORSMiddleware())
 
 	return engine
 }
diff --git a/pkg/blockatlas/collectibles.go b/pkg/blockatlas/collectibles.go
index 74a272e..bad0815 100644
--- a/pkg/blockatlas/collectibles.go
+++ b/pkg/blockatlas/collectibles.go
@@ -1,7 +1,7 @@
 package blockatlas
 
 type (
-	Collection struct {
+	CollectionV3 struct {
 		Id              string `json:"id"`
 		Name            string `json:"name"`
 		Symbol          string `json:"symbol"`
@@ -18,14 +18,26 @@ type (
 		Type    string `json:"type"`
 	}
 
+	Collection struct {
+		Id           string `json:"id"`
+		Name         string `json:"name"`
+		ImageUrl     string `json:"image_url"`
+		Description  string `json:"description"`
+		ExternalLink string `json:"external_link"`
+		Total        int    `json:"total"`
+		Address      string `json:"address"`
+		Coin         uint   `json:"coin"`
+		Type         string `json:"-"`
+	}
+
+	CollectionPageV3 []CollectionV3
+
 	CollectionPage []Collection
 
 	Collectible struct {
-		ID               string `json:"id"`
-		CollectionID     string `json:"collection_id"`
-		TokenID          string `json:"token_id"`
-		CategoryContract string `json:"category_contract"`
-		// Deprecated: for support old client, ContractAddress eq CollectionID
+		ID              string `json:"id"`
+		CollectionID    string `json:"collection_id"`
+		TokenID         string `json:"token_id"`
 		ContractAddress string `json:"contract_address"`
 		Category        string `json:"category"`
 		ImageUrl        string `json:"image_url"`
@@ -39,4 +51,23 @@ type (
 	}
 
 	CollectiblePage []Collectible
+
+	CollectibleV3 struct {
+		ID               string `json:"id"`
+		CollectionID     string `json:"collection_id"`
+		TokenID          string `json:"token_id"`
+		CategoryContract string `json:"category_contract"`
+		ContractAddress  string `json:"contract_address"`
+		Category         string `json:"category"`
+		ImageUrl         string `json:"image_url"`
+		ExternalLink     string `json:"external_link"`
+		ProviderLink     string `json:"provider_link"`
+		Type             string `json:"type"`
+		Description      string `json:"description"`
+		Coin             uint   `json:"coin"`
+		Name             string `json:"name"`
+		Version          string `json:"nft_version"`
+	}
+
+	CollectiblePageV3 []CollectibleV3
 )
diff --git a/pkg/blockatlas/marshal.go b/pkg/blockatlas/marshal.go
index 00f6dd8..91b9a54 100644
--- a/pkg/blockatlas/marshal.go
+++ b/pkg/blockatlas/marshal.go
@@ -163,3 +163,35 @@ func (r CollectiblePage) MarshalJSON() ([]byte, error) {
 	page.Status = true
 	return json.Marshal(page)
 }
+
+// MarshalJSON returns a wrapped list of collections in JSON
+func (r CollectionPageV3) MarshalJSON() ([]byte, error) {
+	var page struct {
+		Total  int            `json:"total"`
+		Docs   []CollectionV3 `json:"docs"`
+		Status bool           `json:"status"`
+	}
+	page.Docs = []CollectionV3(r)
+	if page.Docs == nil {
+		page.Docs = make([]CollectionV3, 0)
+	}
+	page.Total = len(page.Docs)
+	page.Status = true
+	return json.Marshal(page)
+}
+
+// MarshalJSON returns a wrapped list of collectibles in JSON
+func (r CollectiblePageV3) MarshalJSON() ([]byte, error) {
+	var page struct {
+		Total  int             `json:"total"`
+		Docs   []CollectibleV3 `json:"docs"`
+		Status bool            `json:"status"`
+	}
+	page.Docs = []CollectibleV3(r)
+	if page.Docs == nil {
+		page.Docs = make([]CollectibleV3, 0)
+	}
+	page.Total = len(page.Docs)
+	page.Status = true
+	return json.Marshal(page)
+}
diff --git a/pkg/blockatlas/platform.go b/pkg/blockatlas/platform.go
index 28bbbb6..43c23e8 100644
--- a/pkg/blockatlas/platform.go
+++ b/pkg/blockatlas/platform.go
@@ -56,11 +56,8 @@ type (
 		GetCollections(owner string) (CollectionPage, error)
 		GetCollectibles(owner, collectibleID string) (CollectiblePage, error)
 
-		OldGetCollections(owner string) (CollectionPage, error)
-		OldGetCollectibles(owner, collectibleID string) (CollectiblePage, error)
-
-		GetCollectionsV4(owner string) (CollectionPage, error)
-		GetCollectiblesV4(owner, collectibleID string) (CollectiblePage, error)
+		GetCollectionsV3(owner string) (CollectionPageV3, error)
+		GetCollectiblesV3(owner, collectibleID string) (CollectiblePageV3, error)
 	}
 
 	// CustomAPI provides custom HTTP routes
diff --git a/pkg/ginutils/response.go b/pkg/ginutils/response.go
deleted file mode 100644
index 83efac6..0000000
--- a/pkg/ginutils/response.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package ginutils
-
-import (
-	"encoding/json"
-	"github.com/gin-gonic/gin"
-	"net/http"
-)
-
-type ApiError struct {
-	*gin.Context  `json:"-"`
-	StatusCode    int    `json:"status_code"`
-	StatusMessage string `json:"status_message"`
-}
-
-func RenderSuccess(c *gin.Context, result interface{}) {
-	c.JSON(http.StatusOK, result)
-}
-
-func RenderError(c *gin.Context, code int, msg string) {
-	err := &ApiError{
-		StatusCode:    code,
-		StatusMessage: msg,
-		Context:       c,
-	}
-	err.Render()
-}
-
-func ErrorResponse(c *gin.Context) *ApiError {
-	return &ApiError{
-		StatusCode:    http.StatusInternalServerError,
-		StatusMessage: "Internal server error",
-		Context:       c,
-	}
-}
-
-func (e *ApiError) Code(code int) *ApiError {
-	e.StatusCode = code
-	return e
-}
-
-func (e *ApiError) Message(msg string) *ApiError {
-	e.StatusMessage = msg
-	return e
-}
-
-func (e *ApiError) Params(code int, msg string) *ApiError {
-	e.StatusCode = code
-	e.StatusMessage = msg
-	return e
-}
-
-func (e *ApiError) Render() {
-	var msg map[string]interface{}
-	err := json.Unmarshal([]byte(e.StatusMessage), &msg)
-	if err == nil {
-		e.AbortWithStatusJSON(e.StatusCode, map[string]interface{}{
-			"error": msg,
-			"code":  e.StatusCode,
-		})
-		return
-	}
-	e.AbortWithStatusJSON(e.StatusCode, map[string]interface{}{
-		"error": e.StatusMessage,
-		"code":  e.StatusCode,
-	})
-}
diff --git a/platform/ethereum/collection.go b/platform/ethereum/collection.go
index 251df61..7cb836c 100644
--- a/platform/ethereum/collection.go
+++ b/platform/ethereum/collection.go
@@ -1,14 +1,12 @@
 package ethereum
 
 import (
-	"fmt"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
 	"strings"
 )
 
 var (
 	supportedTypes = map[string]bool{"ERC721": true, "ERC1155": true}
-	slugTokens     = map[string]bool{"ERC1155": true}
 )
 
 func (p *Platform) GetCollections(owner string) (blockatlas.CollectionPage, error) {
@@ -16,252 +14,63 @@ func (p *Platform) GetCollections(owner string) (blockatlas.CollectionPage, erro
 	if err != nil {
 		return nil, err
 	}
-	page := NormalizeCollectionPage(collections, p.CoinIndex, owner)
-	return page, nil
-}
-
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func (p *Platform) OldGetCollections(owner string) (blockatlas.CollectionPage, error) {
-	collections, err := p.collectionsClient.GetCollections(owner)
-	if err != nil {
-		return nil, err
-	}
-	page := OldNormalizeCollectionPage(collections, p.CoinIndex, owner)
-	return page, nil
+	return NormalizeCollections(collections, p.CoinIndex, owner), nil
 }
 
 func (p *Platform) GetCollectibles(owner, collectibleID string) (blockatlas.CollectiblePage, error) {
-	collection, items, err := p.collectionsClient.GetCollectibles(owner, collectibleID)
+	items, err := p.collectionsClient.GetCollectibles(owner, collectibleID)
 	if err != nil {
 		return nil, err
 	}
-	page := NormalizeCollectiblePage(collection, items, p.CoinIndex)
-	return page, nil
+	return NormalizeCollectiblePage(items, p.CoinIndex), nil
 }
 
-func NormalizeCollectionPage(collections []Collection, coinIndex uint, owner string) (page blockatlas.CollectionPage) {
+func NormalizeCollections(collections []Collection, coinIndex uint, owner string) (page blockatlas.CollectionPage) {
 	for _, collection := range collections {
-		if len(collection.Contracts) == 0 {
-			continue
-		}
 		item := NormalizeCollection(collection, coinIndex, owner)
-		if _, ok := supportedTypes[item.Type]; !ok {
-			continue
-		}
 		page = append(page, item)
 	}
-	return
-}
-
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func (p *Platform) OldGetCollectibles(owner, collectibleID string) (blockatlas.CollectiblePage, error) {
-	collection, items, err := p.collectionsClient.OldGetCollectibles(owner, collectibleID)
-	if err != nil {
-		return nil, err
-	}
-	page := OldNormalizeCollectiblePage(collection, items, p.CoinIndex)
-	return page, nil
-}
-
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func OldNormalizeCollectionPage(collections []Collection, coinIndex uint, owner string) (page blockatlas.CollectionPage) {
-	for _, collection := range collections {
-		if len(collection.Contracts) == 0 {
-			continue
-		}
-		item := OldNormalizeCollection(collection, coinIndex, owner)
-		if _, ok := supportedTypes[item.Type]; !ok {
-			continue
-		}
-		page = append(page, item)
-	}
-	return
-}
-
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func OldNormalizeCollection(c Collection, coinIndex uint, owner string) blockatlas.Collection {
-	if len(c.Contracts) == 0 {
-		return blockatlas.Collection{}
-	}
-
-	description := blockatlas.GetValidParameter(c.Description, c.Contracts[0].Description)
-	symbol := blockatlas.GetValidParameter(c.Contracts[0].Symbol, "")
-	collectionId := blockatlas.GetValidParameter(c.Contracts[0].Address, "")
-	version := blockatlas.GetValidParameter(c.Contracts[0].NftVersion, "")
-	collectionType := blockatlas.GetValidParameter(c.Contracts[0].Type, "")
-	if _, ok := slugTokens[collectionType]; ok {
-		collectionId = createCollectionId(collectionId, c.Slug)
-	}
-
-	return blockatlas.Collection{
-		Name:            c.Name,
-		Symbol:          symbol,
-		Slug:            c.Slug,
-		ImageUrl:        c.ImageUrl,
-		Description:     description,
-		ExternalLink:    c.ExternalUrl,
-		Total:           int(c.Total.Int64()),
-		Id:              collectionId,
-		CategoryAddress: collectionId,
-		Address:         owner,
-		Version:         version,
-		Coin:            coinIndex,
-		Type:            collectionType,
-	}
-}
-
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func OldNormalizeCollectible(c *Collection, a Collectible, coinIndex uint) blockatlas.Collectible {
-	address := blockatlas.GetValidParameter(c.Contracts[0].Address, "")
-	collectionType := blockatlas.GetValidParameter(c.Contracts[0].Type, "")
-	collectionID := address
-	if _, ok := slugTokens[collectionType]; ok {
-		collectionID = createCollectionId(address, c.Slug)
-	}
-	externalLink := blockatlas.GetValidParameter(a.ExternalLink, a.AssetContract.ExternalLink)
-	id := strings.Join([]string{a.AssetContract.Address, a.TokenId}, "-")
-	return blockatlas.Collectible{
-		ID:               id,
-		CollectionID:     collectionID,
-		ContractAddress:  address,
-		TokenID:          a.TokenId,
-		CategoryContract: a.AssetContract.Address,
-		Name:             a.Name,
-		Category:         c.Name,
-		ImageUrl:         a.ImagePreviewUrl,
-		ProviderLink:     a.Permalink,
-		ExternalLink:     externalLink,
-		Type:             collectionType,
-		Description:      a.Description,
-		Coin:             coinIndex,
-	}
-}
-
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func createCollectionId(address, slug string) string {
-	return fmt.Sprintf("%s---%s", address, slug)
-}
-
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func getCollectionId(collectionId string) string {
-	s := strings.Split(collectionId, "---")
-	if len(s) != 2 {
-		return collectionId
-	}
-	return s[1]
-}
-
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func OldNormalizeCollectiblePage(c *Collection, srcPage []Collectible, coinIndex uint) (page blockatlas.CollectiblePage) {
-	if len(c.Contracts) == 0 {
-		return
-	}
-	for _, src := range srcPage {
-		item := OldNormalizeCollectible(c, src, coinIndex)
-		if _, ok := supportedTypes[item.Type]; !ok {
-			continue
-		}
-		page = append(page, item)
-	}
-	return
+	return page
 }
 
 func NormalizeCollection(c Collection, coinIndex uint, owner string) blockatlas.Collection {
-	normalizeSupportedContracts(&c)
-	if len(c.Contracts) == 0 {
-		return blockatlas.Collection{}
-	}
-
-	description := blockatlas.GetValidParameter(c.Description, c.Contracts[0].Description)
-	symbol := blockatlas.GetValidParameter(c.Contracts[0].Symbol, "")
-	version := blockatlas.GetValidParameter(c.Contracts[0].NftVersion, "")
-	collectionType := blockatlas.GetValidParameter(c.Contracts[0].Type, "")
-
 	return blockatlas.Collection{
-		Name:            c.Name,
-		Symbol:          symbol,
-		Slug:            c.Slug,
-		ImageUrl:        c.ImageUrl,
-		Description:     description,
-		ExternalLink:    c.ExternalUrl,
-		Total:           int(c.Total.Int64()),
-		Id:              c.Slug,
-		CategoryAddress: c.Slug,
-		Address:         owner,
-		Version:         version,
-		Coin:            coinIndex,
-		Type:            collectionType,
-	}
-}
-
-func normalizeSupportedContracts(c *Collection) {
-	supportedContracts := make([]PrimaryAssetContract, 0)
-	for _, contract := range c.Contracts {
-		if _, ok := supportedTypes[contract.Type]; !ok {
-			continue
+		Name:         c.Name,
+		ImageUrl:     c.ImageUrl,
+		Description:  c.Description,
+		ExternalLink: c.ExternalUrl,
+		Total:        int(c.Total.Int64()),
+		Id:           c.Slug,
+		Address:      owner,
+		Coin:         coinIndex,
+	}
+}
+
+func NormalizeCollectiblePage(collectibles []Collectible, coinIndex uint) (page blockatlas.CollectiblePage) {
+	for _, collectible := range collectibles {
+		item := NormalizeCollectible(collectible, coinIndex)
+		if _, ok := supportedTypes[item.Type]; ok {
+			page = append(page, item)
 		}
-		supportedContracts = append(supportedContracts, contract)
 	}
-	c.Contracts = supportedContracts
+	return page
 }
 
-func NormalizeCollectiblePage(c *Collection, srcPage []Collectible, coinIndex uint) (page blockatlas.CollectiblePage) {
-	normalizeSupportedContracts(c)
-	if len(c.Contracts) == 0 {
-		return
-	}
-	for _, src := range srcPage {
-		item := NormalizeCollectible(c, src, coinIndex)
-		if _, ok := supportedTypes[item.Type]; !ok {
-			continue
-		}
-		page = append(page, item)
-	}
-	return
-}
-
-func NormalizeCollectible(c *Collection, a Collectible, coinIndex uint) blockatlas.Collectible {
-	address := blockatlas.GetValidParameter(c.Contracts[0].Address, "")
-	collectionType := blockatlas.GetValidParameter(c.Contracts[0].Type, "")
-	externalLink := blockatlas.GetValidParameter(a.ExternalLink, a.AssetContract.ExternalLink)
+func NormalizeCollectible(a Collectible, coinIndex uint) blockatlas.Collectible {
 	id := strings.Join([]string{a.AssetContract.Address, a.TokenId}, "-")
 	return blockatlas.Collectible{
-		ID:               id,
-		CollectionID:     c.Slug,
-		ContractAddress:  address,
-		TokenID:          a.TokenId,
-		CategoryContract: a.AssetContract.Address,
-		Name:             a.Name,
-		Category:         c.Name,
-		ImageUrl:         a.ImagePreviewUrl,
-		ProviderLink:     a.Permalink,
-		ExternalLink:     externalLink,
-		Type:             collectionType,
-		Description:      a.Description,
-		Coin:             coinIndex,
-	}
-}
-
-func searchCollection(collections []Collection, collectibleID string) *Collection {
-	for _, i := range collections {
-		if strings.EqualFold(i.Slug, collectibleID) {
-			return &i
-		}
-	}
-	return nil
-}
-
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func oldSearchCollection(collections []Collection, collectibleID string) *Collection {
-	for _, i := range collections {
-		if strings.EqualFold(i.Slug, collectibleID) {
-			return &i
-		}
-		for _, contract := range i.Contracts {
-			if strings.EqualFold(contract.Address, collectibleID) {
-				return &i
-			}
-		}
+		ID:              id,
+		CollectionID:    a.Collection.Slug,
+		TokenID:         a.TokenId,
+		ContractAddress: a.AssetContract.Address,
+		Name:            a.Name,
+		Category:        a.Collection.Name,
+		ImageUrl:        a.ImagePreviewUrl,
+		ProviderLink:    a.Permalink,
+		ExternalLink:    a.Collection.ExternalLink,
+		Type:            a.AssetContract.Type,
+		Description:     a.Description,
+		Coin:            coinIndex,
+		Version:         a.AssetContract.Version,
 	}
-	return nil
 }
diff --git a/platform/ethereum/collectionV3.go b/platform/ethereum/collectionV3.go
new file mode 100644
index 0000000..dc35a5a
--- /dev/null
+++ b/platform/ethereum/collectionV3.go
@@ -0,0 +1,114 @@
+package ethereum
+
+import (
+	"github.com/trustwallet/blockatlas/pkg/blockatlas"
+	"strings"
+)
+
+func (p *Platform) GetCollectionsV3(owner string) (blockatlas.CollectionPageV3, error) {
+	collections, err := p.collectionsClient.GetCollections(owner)
+	if err != nil {
+		return nil, err
+	}
+	page := NormalizeCollectionPageV3(collections, p.CoinIndex, owner)
+	return page, nil
+}
+
+func (p *Platform) GetCollectiblesV3(owner, collectibleID string) (blockatlas.CollectiblePageV3, error) {
+	collection, items, err := p.collectionsClient.GetCollectiblesV3(owner, collectibleID)
+	if err != nil {
+		return nil, err
+	}
+	page := NormalizeCollectiblePageV3(collection, items, p.CoinIndex)
+	return page, nil
+}
+
+func NormalizeCollectionPageV3(collections []Collection, coinIndex uint, owner string) (page blockatlas.CollectionPageV3) {
+	for _, collection := range collections {
+		if len(collection.Contracts) == 0 {
+			continue
+		}
+		item := NormalizeCollectionV3(collection, coinIndex, owner)
+		if _, ok := supportedTypes[item.Type]; !ok {
+			continue
+		}
+		page = append(page, item)
+	}
+	return
+}
+
+func NormalizeCollectionV3(c Collection, coinIndex uint, owner string) blockatlas.CollectionV3 {
+	normalizeSupportedContracts(&c)
+	if len(c.Contracts) == 0 {
+		return blockatlas.CollectionV3{}
+	}
+
+	description := blockatlas.GetValidParameter(c.Description, c.Contracts[0].Description)
+	symbol := blockatlas.GetValidParameter(c.Contracts[0].Symbol, "")
+	version := blockatlas.GetValidParameter(c.Contracts[0].NftVersion, "")
+	collectionType := blockatlas.GetValidParameter(c.Contracts[0].Type, "")
+
+	return blockatlas.CollectionV3{
+		Name:            c.Name,
+		Symbol:          symbol,
+		Slug:            c.Slug,
+		ImageUrl:        c.ImageUrl,
+		Description:     description,
+		ExternalLink:    c.ExternalUrl,
+		Total:           int(c.Total.Int64()),
+		Id:              c.Slug,
+		CategoryAddress: c.Slug,
+		Address:         owner,
+		Version:         version,
+		Coin:            coinIndex,
+		Type:            collectionType,
+	}
+}
+
+func normalizeSupportedContracts(c *Collection) {
+	supportedContracts := make([]PrimaryAssetContract, 0)
+	for _, contract := range c.Contracts {
+		if _, ok := supportedTypes[contract.Type]; !ok {
+			continue
+		}
+		supportedContracts = append(supportedContracts, contract)
+	}
+	c.Contracts = supportedContracts
+}
+
+func NormalizeCollectiblePageV3(c *Collection, srcPage []Collectible, coinIndex uint) (page blockatlas.CollectiblePageV3) {
+	normalizeSupportedContracts(c)
+	if len(c.Contracts) == 0 {
+		return
+	}
+	for _, src := range srcPage {
+		item := NormalizeCollectibleV3(c, src, coinIndex)
+		if _, ok := supportedTypes[item.Type]; !ok {
+			continue
+		}
+		page = append(page, item)
+	}
+	return
+}
+
+func NormalizeCollectibleV3(c *Collection, a Collectible, coinIndex uint) blockatlas.CollectibleV3 {
+	address := blockatlas.GetValidParameter(c.Contracts[0].Address, "")
+	collectionType := blockatlas.GetValidParameter(c.Contracts[0].Type, "")
+	externalLink := blockatlas.GetValidParameter(a.ExternalLink, a.AssetContract.ExternalLink)
+	id := strings.Join([]string{a.AssetContract.Address, a.TokenId}, "-")
+	return blockatlas.CollectibleV3{
+		ID:               id,
+		CollectionID:     c.Slug,
+		ContractAddress:  address,
+		TokenID:          a.TokenId,
+		CategoryContract: a.AssetContract.Address,
+		Name:             a.Name,
+		Category:         c.Name,
+		ImageUrl:         a.ImagePreviewUrl,
+		ProviderLink:     a.Permalink,
+		ExternalLink:     externalLink,
+		Type:             collectionType,
+		Description:      a.Description,
+		Coin:             coinIndex,
+	}
+}
diff --git a/platform/ethereum/collection_client.go b/platform/ethereum/collection_client.go
index 2c4bdeb..5a049e7 100644
--- a/platform/ethereum/collection_client.go
+++ b/platform/ethereum/collection_client.go
@@ -5,6 +5,7 @@ import (
 	"github.com/trustwallet/blockatlas/pkg/errors"
 	"net/url"
 	"strconv"
+	"strings"
 )
 
 type CollectionsClient struct {
@@ -20,30 +21,7 @@ func (c CollectionsClient) GetCollections(owner string) (page []Collection, err
 	return
 }
 
-func (c CollectionsClient) GetCollectibles(owner string, collectibleID string) (*Collection, []Collectible, error) {
-	collections, err := c.GetCollections(owner)
-	if err != nil {
-		return nil, nil, err
-	}
-	collection := searchCollection(collections, collectibleID)
-	if collection == nil {
-		return nil, nil, errors.E("collectible not found", errors.TypePlatformClient,
-			errors.Params{"collectibleID": collectibleID})
-	}
-
-	query := url.Values{
-		"owner": {owner},
-		"limit": {strconv.Itoa(300)},
-	}
-
-	query.Set("collection", collection.Slug)
-
-	var page CollectiblePage
-	err = c.Get(&page, "api/v1/assets", query)
-	return collection, page.Collectibles, err
-}
-
-func (c CollectionsClient) GetCollectiblesV4(owner string, collectibleID string) ([]Collectible, error) {
+func (c CollectionsClient) GetCollectibles(owner string, collectibleID string) ([]Collectible, error) {
 	query := url.Values{
 		"owner":      {owner},
 		"collection": {collectibleID},
@@ -55,14 +33,12 @@ func (c CollectionsClient) GetCollectiblesV4(owner string, collectibleID string)
 	return page.Collectibles, err
 }
 
-//TODO: remove once most of the clients will be updated (deadline: March 17th)
-func (c CollectionsClient) OldGetCollectibles(owner string, collectibleID string) (*Collection, []Collectible, error) {
+func (c CollectionsClient) GetCollectiblesV3(owner string, collectibleID string) (*Collection, []Collectible, error) {
 	collections, err := c.GetCollections(owner)
 	if err != nil {
 		return nil, nil, err
 	}
-	id := getCollectionId(collectibleID)
-	collection := oldSearchCollection(collections, id)
+	collection := searchCollection(collections, collectibleID)
 	if collection == nil {
 		return nil, nil, errors.E("collectible not found", errors.TypePlatformClient,
 			errors.Params{"collectibleID": collectibleID})
@@ -73,15 +49,18 @@ func (c CollectionsClient) OldGetCollectibles(owner string, collectibleID string
 		"limit": {strconv.Itoa(300)},
 	}
 
-	for _, i := range collection.Contracts {
-		if _, ok := slugTokens[i.Type]; ok {
-			query.Set("collection", collection.Slug)
-			break
-		}
-		query.Add("asset_contract_addresses", i.Address)
-	}
+	query.Set("collection", collection.Slug)
 
 	var page CollectiblePage
 	err = c.Get(&page, "api/v1/assets", query)
 	return collection, page.Collectibles, err
 }
+
+func searchCollection(collections []Collection, collectibleID string) *Collection {
+	for _, i := range collections {
+		if strings.EqualFold(i.Slug, collectibleID) {
+			return &i
+		}
+	}
+	return nil
+}
diff --git a/platform/ethereum/collection_v4.go b/platform/ethereum/collection_v4.go
deleted file mode 100644
index 2eed537..0000000
--- a/platform/ethereum/collection_v4.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package ethereum
-
-import (
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"strings"
-)
-
-func (p *Platform) GetCollectionsV4(owner string) (blockatlas.CollectionPage, error) {
-	collections, err := p.collectionsClient.GetCollections(owner)
-	if err != nil {
-		return nil, err
-	}
-	return NormalizeCollectionsV4(collections, p.CoinIndex, owner), nil
-}
-
-func (p *Platform) GetCollectiblesV4(owner, collectibleID string) (blockatlas.CollectiblePage, error) {
-	items, err := p.collectionsClient.GetCollectiblesV4(owner, collectibleID)
-	if err != nil {
-		return nil, err
-	}
-	return NormalizeCollectiblePageV4(items, p.CoinIndex), nil
-}
-
-func NormalizeCollectionsV4(collections []Collection, coinIndex uint, owner string) (page blockatlas.CollectionPage) {
-	for _, collection := range collections {
-		item := NormalizeCollectionV4(collection, coinIndex, owner)
-		page = append(page, item)
-	}
-	return page
-}
-
-func NormalizeCollectionV4(c Collection, coinIndex uint, owner string) blockatlas.Collection {
-	return blockatlas.Collection{
-		Name:         c.Name,
-		Slug:         c.Slug,
-		ImageUrl:     c.ImageUrl,
-		Description:  c.Description,
-		ExternalLink: c.ExternalUrl,
-		Total:        int(c.Total.Int64()),
-		Id:           c.Slug,
-		Address:      owner,
-		Coin:         coinIndex,
-	}
-}
-
-func NormalizeCollectiblePageV4(collectibles []Collectible, coinIndex uint) (page blockatlas.CollectiblePage) {
-	for _, collectible := range collectibles {
-		item := NormalizeCollectibleV4(collectible, coinIndex)
-		if _, ok := supportedTypes[item.Type]; ok {
-			page = append(page, item)
-		}
-	}
-	return page
-}
-
-func NormalizeCollectibleV4(a Collectible, coinIndex uint) blockatlas.Collectible {
-	id := strings.Join([]string{a.AssetContract.Address, a.TokenId}, "-")
-	return blockatlas.Collectible{
-		ID:              id,
-		CollectionID:    a.Collection.Slug,
-		TokenID:         a.TokenId,
-		ContractAddress: a.AssetContract.Address,
-		Name:            a.Name,
-		Category:        a.Collection.Name,
-		ImageUrl:        a.ImagePreviewUrl,
-		ProviderLink:    a.Permalink,
-		ExternalLink:    a.Collection.ExternalLink,
-		Type:            a.AssetContract.Type,
-		Description:     a.Description,
-		Coin:            coinIndex,
-		Version:         a.AssetContract.Version,
-	}
-}


Test patch:
diff --git a/pkg/ginutils/gincache/cache_test.go b/api/middleware/cache_test.go
similarity index 73%
rename from pkg/ginutils/gincache/cache_test.go
rename to api/middleware/cache_test.go
index 9467aca..c2cda6a 100644
--- a/pkg/ginutils/gincache/cache_test.go
+++ b/api/middleware/cache_test.go
@@ -1,10 +1,10 @@
-package gincache
+package middleware
 
 import (
 	"fmt"
 	"github.com/gin-gonic/gin"
 	"github.com/stretchr/testify/assert"
-	"github.com/trustwallet/blockatlas/pkg/ginutils"
+	"net/http"
 	"net/http/httptest"
 	"testing"
 	"time"
@@ -28,10 +28,10 @@ func TestWrite(t *testing.T) {
 	writer := newCachedWriter(time.Second*3, c.Writer, "mykey")
 	c.Writer = writer
 
-	c.Writer.WriteHeader(204)
+	c.Writer.WriteHeader(http.StatusNoContent)
 	c.Writer.WriteHeaderNow()
-	c.Writer.Write([]byte("foo")) // nolint
-	assert.Equal(t, 204, c.Writer.Status())
+	_, _ = c.Writer.Write([]byte("foo")) // nolint
+	assert.Equal(t, http.StatusNoContent, c.Writer.Status())
 	assert.Equal(t, "foo", w.Body.String())
 	assert.True(t, c.Writer.Written())
 }
@@ -39,28 +39,28 @@ func TestWrite(t *testing.T) {
 func TestCachePage(t *testing.T) {
 	router := gin.New()
 	router.GET("/cache_ping", CacheMiddleware(time.Second*3, func(c *gin.Context) {
-		ginutils.RenderSuccess(c, "pong "+fmt.Sprint(time.Now().UnixNano()))
+		c.JSON(http.StatusOK, "pong "+fmt.Sprint(time.Now().UnixNano()))
 	}))
 
 	w1 := performRequest("GET", "/cache_ping", router)
 	w2 := performRequest("GET", "/cache_ping", router)
 
-	assert.Equal(t, 200, w1.Code)
-	assert.Equal(t, 200, w2.Code)
+	assert.Equal(t, http.StatusOK, w1.Code)
+	assert.Equal(t, http.StatusOK, w2.Code)
 	assert.Equal(t, w1.Body.String(), w2.Body.String())
 }
 
 func TestCachePageExpire(t *testing.T) {
 	router := gin.New()
 	router.GET("/cache_ping", CacheMiddleware(time.Second, func(c *gin.Context) {
-		ginutils.RenderSuccess(c, "pong "+fmt.Sprint(time.Now().UnixNano()))
+		c.JSON(http.StatusOK, "pong "+fmt.Sprint(time.Now().UnixNano()))
 	}))
 
 	w1 := performRequest("GET", "/cache_ping", router)
 	time.Sleep(time.Second * 3)
 	w2 := performRequest("GET", "/cache_ping", router)
 
-	assert.Equal(t, 200, w1.Code)
-	assert.Equal(t, 200, w2.Code)
+	assert.Equal(t, http.StatusOK, w1.Code)
+	assert.Equal(t, http.StatusOK, w2.Code)
 	assert.NotEqual(t, w1.Body.String(), w2.Body.String())
 }
diff --git a/platform/ethereum/collection_test.go b/platform/ethereum/collection_test.go
index 056743e..771baa5 100644
--- a/platform/ethereum/collection_test.go
+++ b/platform/ethereum/collection_test.go
@@ -2,142 +2,282 @@ package ethereum
 
 import (
 	"encoding/json"
-	"fmt"
 	"github.com/stretchr/testify/assert"
 	"github.com/trustwallet/blockatlas/coin"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"math/big"
 	"testing"
 )
 
-const collectionsOwner = "0x0875BCab22dE3d02402bc38aEe4104e1239374a7"
+const collectionsOwnerV4 = "0x0875BCab22dE3d02402bc38aEe4104e1239374a7"
 
-const collectionsSrc = `
+const collectionsSrcV4 = `
 [
-  {
-    "primary_asset_contracts": [
-      {
-        "address": "0x06012c8cf97bead5deae237070f9587f8e7a266d",
-        "name": "CryptoKitties",
-        "symbol": "CKITTY",
-        "description": "CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
-        "external_link": "https://www.cryptokitties.co/",
-        "nft_version": "1.0",
-        "schema_name": "ERC721",
-        "display_data": {
-          "images": [
-            "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/564155.svg",
-            "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/546630.svg",
-            "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/441529.svg",
-            "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/552435.svg",
-            "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/524748.png",
-            "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/540800.svg"
-          ],
-          "card_display_style": "padded"
-        },
-        "image_url": "https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556588705.png"
-      }
-    ],
-    "name": "CryptoKitties",
-    "slug": "cryptokitties",
-    "image_url": "https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556588705.png",
-    "description": "CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
-    "external_url": "https://www.cryptokitties.co/",
-    "featured_image_url": "https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556589429.png",
-    "created_date": "2019-04-26T22:13:04.207050",
-    "owned_asset_count": 3
-  },
-  {
-    "primary_asset_contracts": [
-      {
-        "address": "0xf629cbd94d3791c9250152bd8dfbdf380e2a3b9c",
-        "name": "Coin",
-        "symbol": "",
-        "description": null,
-        "external_link": null,
-        "nft_version": null,
-        "schema_name": "ERC20",
-        "display_data": {}
-      }
-    ],
-    "name": "Enjin Token",
-    "slug": "enjin-token",
-    "image_url": "https://storage.googleapis.com/opensea-static/tokens-high-res/ENJ.png",
-    "description": "This is the collection of owners of EnjinCoin",
-    "external_url": null,
-    "owned_asset_count": 20000000000000000000
-  },
-  {
-    "primary_asset_contracts": [
-      {
-        "address": "0xfaafdc07907ff5120a76b34b731b278c38d6043c",
-        "name": "Enjin",
-        "symbol": "",
-        "description": "",
-        "external_link": null,
-        "nft_version": null,
-        "schema_name": "ERC1155",
-        "display_data": {},
-        "owner": null,
-        "created_date": "2019-08-02T23:43:14.666153",
-        "asset_contract_type": "semi-fungible"
-      }
-    ],
-    "name": "Age of Rust",
-    "slug": "age-of-rust",
-    "image_url": "https://storage.opensea.io/age-of-rust-1561960816.jpg",
-    "description": "Year 4424: The search begins for new life on the other side of the galaxy. Explore abandoned space stations, mysterious caverns, and ruins on far away worlds in order to unlock puzzles and secrets! Beware the rogue machines!",
-    "external_url": "https://www.ageofrust.games/",
-    "featured_image_url": null,
-    "created_date": "2019-09-03T02:35:56.063685",
-    "owned_asset_count": 1
-  }
+   {
+      "primary_asset_contracts":[
+         {
+            "address":"0x06012c8cf97bead5deae237070f9587f8e7a266d",
+            "name":"CryptoKitties",
+            "symbol":"CKITTY",
+            "description":"CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
+            "external_link":"https://www.cryptokitties.co/",
+            "nft_version":"1.0",
+            "schema_name":"ERC721",
+            "display_data":{
+               "images":[
+                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/564155.svg",
+                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/546630.svg",
+                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/441529.svg",
+                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/552435.svg",
+                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/524748.png",
+                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/540800.svg"
+               ],
+               "card_display_style":"padded"
+            },
+            "image_url":"https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556588705.png"
+         }
+      ],
+      "name":"CryptoKitties",
+      "slug":"cryptokitties",
+      "image_url":"https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556588705.png",
+      "description":"CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
+      "external_url":"https://www.cryptokitties.co/",
+      "featured_image_url":"https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556589429.png",
+      "created_date":"2019-04-26T22:13:04.207050",
+      "owned_asset_count":3
+   },
+   {
+      "primary_asset_contracts":[
+         {
+            "address":"0xfaafdc07907ff5120a76b34b731b278c38d6043c",
+            "name":"Enjin",
+            "symbol":"",
+            "description":"",
+            "external_link":null,
+            "nft_version":null,
+            "schema_name":"ERC1155",
+            "display_data":{
+
+            },
+            "owner":null,
+            "created_date":"2019-08-02T23:43:14.666153",
+            "asset_contract_type":"semi-fungible"
+         }
+      ],
+      "name":"Age of Rust",
+      "slug":"age-of-rust",
+      "image_url":"https://storage.opensea.io/age-of-rust-1561960816.jpg",
+      "description":"Year 4424: The search begins for new life on the other side of the galaxy. Explore abandoned space stations, mysterious caverns, and ruins on far away worlds in order to unlock puzzles and secrets! Beware the rogue machines!",
+      "external_url":"https://www.ageofrust.games/",
+      "featured_image_url":null,
+      "created_date":"2019-09-03T02:35:56.063685",
+      "owned_asset_count":1
+   },
+   {
+      "primary_asset_contracts":[
+         {
+            "address":"0xee85966b4974d3c6f71a2779cc3b6f53afbc2b68",
+            "asset_contract_type":"fungible",
+            "created_date":"2019-10-16T07:36:16.102163",
+            "name":"Rare Chest",
+            "nft_version":null,
+            "opensea_version":null,
+            "owner":1610615,
+            "schema_name":"ERC20",
+            "symbol":"",
+            "total_supply":"1",
+            "description":"Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
+            "external_link":"https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
+            "image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
+            "default_to_fiat":false,
+            "dev_buyer_fee_basis_points":0,
+            "dev_seller_fee_basis_points":0,
+            "only_proxied_transfers":false,
+            "opensea_buyer_fee_basis_points":0,
+            "opensea_seller_fee_basis_points":250,
+            "buyer_fee_basis_points":0,
+            "seller_fee_basis_points":250,
+            "payout_address":null
+         },
+         {
+            "address":"0x20d4cec36528e1c4563c1bfbe3de06aba70b22b4",
+            "asset_contract_type":"fungible",
+            "created_date":"2019-10-16T08:06:42.727997",
+            "name":"Legendary Chest",
+            "nft_version":null,
+            "opensea_version":null,
+            "owner":1610615,
+            "schema_name":"ERC20",
+            "symbol":"",
+            "total_supply":"1",
+            "description":"Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
+            "external_link":"https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
+            "image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
+            "default_to_fiat":false,
+            "dev_buyer_fee_basis_points":0,
+            "dev_seller_fee_basis_points":0,
+            "only_proxied_transfers":false,
+            "opensea_buyer_fee_basis_points":0,
+            "opensea_seller_fee_basis_points":250,
+            "buyer_fee_basis_points":0,
+            "seller_fee_basis_points":250,
+            "payout_address":null
+         },
+         {
+            "address":"0x0e3a2a1f2146d86a604adc220b4967a898d7fe07",
+            "asset_contract_type":"non-fungible",
+            "created_date":"2019-11-01T06:39:04.363034",
+            "name":"Gods Unchained Cards",
+            "nft_version":"3.0",
+            "opensea_version":null,
+            "owner":1691695,
+            "schema_name":"ERC721",
+            "symbol":"",
+            "total_supply":"1",
+            "description":"Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
+            "external_link":"https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
+            "image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
+            "default_to_fiat":false,
+            "dev_buyer_fee_basis_points":0,
+            "dev_seller_fee_basis_points":0,
+            "only_proxied_transfers":false,
+            "opensea_buyer_fee_basis_points":0,
+            "opensea_seller_fee_basis_points":250,
+            "buyer_fee_basis_points":0,
+            "seller_fee_basis_points":250,
+            "payout_address":null
+         },
+         {
+            "address":"0x564cb55c655f727b61d9baf258b547ca04e9e548",
+            "asset_contract_type":"non-fungible",
+            "created_date":"2019-10-29T12:28:37.643714",
+            "name":"Gods Unchained",
+            "nft_version":"3.0",
+            "opensea_version":null,
+            "owner":1691695,
+            "schema_name":"ERC721",
+            "symbol":"",
+            "total_supply":"205",
+            "description":"Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
+            "external_link":"https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
+            "image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
+            "default_to_fiat":false,
+            "dev_buyer_fee_basis_points":0,
+            "dev_seller_fee_basis_points":0,
+            "only_proxied_transfers":false,
+            "opensea_buyer_fee_basis_points":0,
+            "opensea_seller_fee_basis_points":250,
+            "buyer_fee_basis_points":0,
+            "seller_fee_basis_points":250,
+            "payout_address":null
+         }
+      ],
+      "traits":{
+         "mana":{
+            "min":1,
+            "max":18
+         },
+         "health":{
+            "min":1,
+            "max":16
+         },
+         "attack":{
+            "min":1,
+            "max":16
+         }
+      },
+      "stats":{
+         "seven_day_volume":270.564697067863,
+         "seven_day_change":-0.014179906699531201,
+         "total_volume":11825.0384171324,
+         "count":6835331,
+         "num_owners":10791,
+         "market_cap":150892.14138332763,
+         "average_price":0.0396514694030496,
+         "items_sold":298233
+      },
+      "banner_image_url":null,
+      "chat_url":null,
+      "created_date":"2019-11-13T03:01:42.051246",
+      "default_to_fiat":false,
+      "description":"Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
+      "dev_buyer_fee_basis_points":"0",
+      "dev_seller_fee_basis_points":"0",
+      "display_data":{
+         "images":[
+            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/25233.png",
+            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/152875.png",
+            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/25669.png",
+            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/9237.png",
+            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/9228.png",
+            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/9231.png"
+         ],
+         "card_display_style":"contain"
+      },
+      "external_url":"https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
+      "featured":true,
+      "featured_image_url":"https://storage.opensea.io/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab-featured-1556589419.png",
+      "hidden":false,
+      "image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
+      "is_subject_to_whitelist":false,
+      "large_image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ",
+      "name":"Gods Unchained",
+      "only_proxied_transfers":false,
+      "opensea_buyer_fee_basis_points":"0",
+      "opensea_seller_fee_basis_points":"250",
+      "payout_address":null,
+      "require_email":false,
+      "short_description":null,
+      "slug":"gods-unchained",
+      "wiki_url":null,
+      "owned_asset_count":535
+   }
 ]
 `
 
-var collection1Dst = blockatlas.Collection{
-	Name:            "CryptoKitties",
-	Symbol:          "CKITTY",
-	Slug:            "cryptokitties",
-	ImageUrl:        "https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556588705.png",
-	Description:     "CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
-	ExternalLink:    "https://www.cryptokitties.co/",
-	Total:           3,
-	CategoryAddress: "cryptokitties",
-	Id:              "cryptokitties",
-	Address:         "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
-	Version:         "1.0",
-	Coin:            60,
-	Type:            "ERC721",
+var collection1DstV4 = blockatlas.Collection{
+	Name:         "CryptoKitties",
+	ImageUrl:     "https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556588705.png",
+	Description:  "CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
+	ExternalLink: "https://www.cryptokitties.co/",
+	Total:        3,
+	Id:           "cryptokitties",
+	Address:      "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
+	Coin:         60,
 }
 
-var collection2Dst = blockatlas.Collection{
-	Name:            "Age of Rust",
-	Symbol:          "",
-	Slug:            "age-of-rust",
-	ImageUrl:        "https://storage.opensea.io/age-of-rust-1561960816.jpg",
-	Description:     "Year 4424: The search begins for new life on the other side of the galaxy. Explore abandoned space stations, mysterious caverns, and ruins on far away worlds in order to unlock puzzles and secrets! Beware the rogue machines!",
-	ExternalLink:    "https://www.ageofrust.games/",
-	Total:           1,
-	CategoryAddress: "age-of-rust",
-	Id:              "age-of-rust",
-	Address:         "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
-	Version:         "",
-	Coin:            60,
-	Type:            "ERC1155",
+var collection2DstV4 = blockatlas.Collection{
+	Name:         "Age of Rust",
+	ImageUrl:     "https://storage.opensea.io/age-of-rust-1561960816.jpg",
+	Description:  "Year 4424: The search begins for new life on the other side of the galaxy. Explore abandoned space stations, mysterious caverns, and ruins on far away worlds in order to unlock puzzles and secrets! Beware the rogue machines!",
+	ExternalLink: "https://www.ageofrust.games/",
+	Total:        1,
+	Id:           "age-of-rust",
+	Address:      "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
+	Coin:         60,
 }
 
-func TestNormalizeCollection(t *testing.T) {
+var collection3DstV4 = blockatlas.Collection{
+	Name:         "Gods Unchained",
+	ImageUrl:     "https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
+	Description:  "Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
+	ExternalLink: "https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
+	Total:        535,
+	Id:           "gods-unchained",
+	Address:      "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
+	Coin:         60,
+}
+
+func TestNormalizeCollectionV4(t *testing.T) {
 	var collections []Collection
-	err := json.Unmarshal([]byte(collectionsSrc), &collections)
+	err := json.Unmarshal([]byte(collectionsSrcV4), &collections)
 	assert.Nil(t, err)
-	page := NormalizeCollectionPage(collections, coin.ETH, collectionsOwner)
-	assert.Equal(t, len(page), 2, "collections could not be normalized")
-	expected := blockatlas.CollectionPage{collection1Dst, collection2Dst}
+	page := NormalizeCollections(collections, coin.ETH, collectionsOwnerV4)
+	assert.Equal(t, 3, len(page), "collections could not be normalized")
+	expected := blockatlas.CollectionPage{collection1DstV4, collection2DstV4, collection3DstV4}
 	assert.Equal(t, page, expected, "collections don't equal")
 }
 
-const collectibleSrc = `
+const collectibleSrcV4 = `
 [
   {
     "token_id": "54277541829991970107421667568590323026590803461896874578610080514640537714688",
@@ -153,259 +293,38 @@ const collectibleSrc = `
       "nft_version": null,
       "schema_name": "ERC1155",
 	  "display_data": {}
+    },
+	"collection": {
+		"slug": "age-of-rust",
+        "name": "Age of Rust",
+        "external_url": "https://opensea.io/"
     },
     "permalink": "https://opensea.io/assets/0xfaafdc07907ff5120a76b34b731b278c38d6043c/54277541829991970107421667568590323026590803461896874578610080514640537714688"
   }
 ]
 `
 
-var collectibleCollectionDst = Collection{
-	Name:        "Age of Rust",
-	ImageUrl:    "https://storage.opensea.io/age-of-rust-1561960816.jpg",
-	Description: "Year 4424: The search begins for new life on the other side of the galaxy. Explore abandoned space stations, mysterious caverns, and ruins on far away worlds in order to unlock puzzles and secrets! Beware the rogue machines!",
-	ExternalUrl: "https://www.ageofrust.games/",
-	Total:       big.NewInt(1),
-	Slug:        "age-of-rust",
-	Contracts: []PrimaryAssetContract{
-		{
-			Name:        "Age of Rust",
-			Address:     "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-			NftVersion:  "",
-			Symbol:      "",
-			Description: "",
-			Type:        "ERC1155",
-			Url:         "",
-		},
-	},
-}
-
-var collectibleDst = blockatlas.Collectible{
-	ID:               "0xfaafdc07907ff5120a76b34b731b278c38d6043c-54277541829991970107421667568590323026590803461896874578610080514640537714688",
-	CollectionID:     "age-of-rust",
-	TokenID:          "54277541829991970107421667568590323026590803461896874578610080514640537714688",
-	CategoryContract: "0xfaafdc07907ff5120a76b34b731b278c38d6043c",
-	ContractAddress:  "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-	Category:         "Age of Rust",
-	ImageUrl:         "https://storage.opensea.io/0xfaafdc07907ff5120a76b34b731b278c38d6043c-preview/54277541829991970107421667568590323026590803461896874578610080514640537714688-1564858807.png",
-	ExternalLink:     "",
-	ProviderLink:     "https://opensea.io/assets/0xfaafdc07907ff5120a76b34b731b278c38d6043c/54277541829991970107421667568590323026590803461896874578610080514640537714688",
-	Type:             "ERC1155",
-	Description:      "Rustbits are the main token of use within the Age of Rust game universe. You need Rustbits to not only play Age of Rust, but also to purchase in-game cryptoitems as well. Rustbits are radioactive rust scraped off of hulls of abandoned ships that are in orbit around a hidden planet, which is also a gas giant. The planet is so radioactive, it damages ships and kills anyone that gets close to it. Getting bits of rust off of ships is highly rare and prized.",
-	Coin:             60,
-	Name:             "Rustbits",
+var collectibleDstV4 = blockatlas.Collectible{
+	ID:              "0xfaafdc07907ff5120a76b34b731b278c38d6043c-54277541829991970107421667568590323026590803461896874578610080514640537714688",
+	CollectionID:    "age-of-rust",
+	TokenID:         "54277541829991970107421667568590323026590803461896874578610080514640537714688",
+	ContractAddress: "0xfaafdc07907ff5120a76b34b731b278c38d6043c",
+	Category:        "Age of Rust",
+	ImageUrl:        "https://storage.opensea.io/0xfaafdc07907ff5120a76b34b731b278c38d6043c-preview/54277541829991970107421667568590323026590803461896874578610080514640537714688-1564858807.png",
+	ExternalLink:    "https://opensea.io/",
+	ProviderLink:    "https://opensea.io/assets/0xfaafdc07907ff5120a76b34b731b278c38d6043c/54277541829991970107421667568590323026590803461896874578610080514640537714688",
+	Type:            "ERC1155",
+	Description:     "Rustbits are the main token of use within the Age of Rust game universe. You need Rustbits to not only play Age of Rust, but also to purchase in-game cryptoitems as well. Rustbits are radioactive rust scraped off of hulls of abandoned ships that are in orbit around a hidden planet, which is also a gas giant. The planet is so radioactive, it damages ships and kills anyone that gets close to it. Getting bits of rust off of ships is highly rare and prized.",
+	Coin:            60,
+	Name:            "Rustbits",
 }
 
-func TestNormalizeCollectible(t *testing.T) {
-	var collectible []Collectible
-	err := json.Unmarshal([]byte(collectibleSrc), &collectible)
+func TestNormalizeCollectibleV4(t *testing.T) {
+	var collectibles []Collectible
+	err := json.Unmarshal([]byte(collectibleSrcV4), &collectibles)
 	assert.Nil(t, err)
-	page := NormalizeCollectiblePage(&collectibleCollectionDst, collectible, coin.ETH)
+	page := NormalizeCollectiblePage(collectibles, coin.ETH)
 	assert.Equal(t, len(page), 1, "collectible could not be normalized")
-	expected := blockatlas.CollectiblePage{collectibleDst}
+	expected := blockatlas.CollectiblePage{collectibleDstV4}
 	assert.Equal(t, page, expected, "collectible don't equal")
 }
-
-var c1 = Collection{
-	Slug: "enjin",
-	Contracts: []PrimaryAssetContract{{
-		Address: "0xfaafdc07907ff5120a76b34b731b278c38d6043c",
-	}},
-}
-var c2 = Collection{
-	Slug: "cryptokitties",
-	Contracts: []PrimaryAssetContract{{
-		Address: "0x5574Cd97432cEd0D7Caf58ac3c4fEDB2061C98fB",
-	}},
-}
-var c3 = Collection{
-	Slug: "age-of-rust",
-	Contracts: []PrimaryAssetContract{{
-		Address: "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
-	}},
-}
-
-func TestSearchCollection(t *testing.T) {
-	tests := []struct {
-		collections   []Collection
-		collectibleID string
-		result        *Collection
-	}{
-		{[]Collection{c1, c2, c3}, "enjin", &c1},
-		{[]Collection{c1, c2, c3}, "cryptokitties", &c2},
-		{[]Collection{c1, c2}, "age-of-rust", nil},
-		{[]Collection{c1, c2, c3}, "age-of-rust", &c3},
-		{[]Collection{c1, c2}, "cryptokitties", &c2},
-		{[]Collection{c1}, "age-of-rust", nil},
-		{[]Collection{c1, c3}, "enjin", &c1},
-	}
-	for i, tt := range tests {
-		t.Run(fmt.Sprintf("searchCollection %d", i), func(t *testing.T) {
-			s := searchCollection(tt.collections, tt.collectibleID)
-			assert.EqualValues(t, s, tt.result)
-		})
-	}
-
-}
-
-func TestNormalizeSupportedContracts(t *testing.T) {
-	var contracts []PrimaryAssetContract
-	err := json.Unmarshal([]byte(rawAssetContracts), &contracts)
-	assert.Nil(t, err)
-	var collection = Collection{}
-	collection.Contracts = contracts
-	normalizeSupportedContracts(&collection)
-	assert.Equal(t, len(collection.Contracts), 2, "normalizeSupportedContracts with incorrect len")
-	var expectedContracts []PrimaryAssetContract
-	err = json.Unmarshal([]byte(rawAssetContractsExpected), &expectedContracts)
-	assert.Nil(t, err)
-	assert.Equal(t, collection.Contracts, expectedContracts, "normalizeSupportedContracts expectedContracts")
-}
-
-const rawAssetContracts = `[
-    {
-        "address": "0xee85966b4974d3c6f71a2779cc3b6f53afbc2b68",
-        "asset_contract_type": "fungible",
-        "created_date": "2019-10-16T07:36:16.102163",
-        "name": "Rare Chest",
-        "nft_version": null,
-        "opensea_version": null,
-        "owner": 1610615,
-        "schema_name": "ERC20",
-        "symbol": "",
-        "total_supply": "1",
-        "description": "Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-        "external_link": "https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-        "image_url": "https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-        "default_to_fiat": false,
-        "dev_buyer_fee_basis_points": 0,
-        "dev_seller_fee_basis_points": 0,
-        "only_proxied_transfers": false,
-        "opensea_buyer_fee_basis_points": 0,
-        "opensea_seller_fee_basis_points": 250,
-        "buyer_fee_basis_points": 0,
-        "seller_fee_basis_points": 250,
-        "payout_address": null
-    },
-    {
-        "address": "0x20d4cec36528e1c4563c1bfbe3de06aba70b22b4",
-        "asset_contract_type": "fungible",
-        "created_date": "2019-10-16T08:06:42.727997",
-        "name": "Legendary Chest",
-        "nft_version": null,
-        "opensea_version": null,
-        "owner": 1610615,
-        "schema_name": "ERC20",
-        "symbol": "",
-        "total_supply": "1",
-        "description": "Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-        "external_link": "https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-        "image_url": "https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-        "default_to_fiat": false,
-        "dev_buyer_fee_basis_points": 0,
-        "dev_seller_fee_basis_points": 0,
-        "only_proxied_transfers": false,
-        "opensea_buyer_fee_basis_points": 0,
-        "opensea_seller_fee_basis_points": 250,
-        "buyer_fee_basis_points": 0,
-        "seller_fee_basis_points": 250,
-        "payout_address": null
-    },
-    {
-        "address": "0x0e3a2a1f2146d86a604adc220b4967a898d7fe07",
-        "asset_contract_type": "non-fungible",
-        "created_date": "2019-11-01T06:39:04.363034",
-        "name": "Gods Unchained Cards",
-        "nft_version": "3.0",
-        "opensea_version": null,
-        "owner": 1691695,
-        "schema_name": "ERC721",
-        "symbol": "",
-        "total_supply": "1",
-        "description": "Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-        "external_link": "https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-        "image_url": "https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-        "default_to_fiat": false,
-        "dev_buyer_fee_basis_points": 0,
-        "dev_seller_fee_basis_points": 0,
-        "only_proxied_transfers": false,
-        "opensea_buyer_fee_basis_points": 0,
-        "opensea_seller_fee_basis_points": 250,
-        "buyer_fee_basis_points": 0,
-        "seller_fee_basis_points": 250,
-        "payout_address": null
-    },
-    {
-        "address": "0x564cb55c655f727b61d9baf258b547ca04e9e548",
-        "asset_contract_type": "non-fungible",
-        "created_date": "2019-10-29T12:28:37.643714",
-        "name": "Gods Unchained",
-        "nft_version": "3.0",
-        "opensea_version": null,
-        "owner": 1691695,
-        "schema_name": "ERC721",
-        "symbol": "",
-        "total_supply": "205",
-        "description": "Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-        "external_link": "https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-        "image_url": "https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-        "default_to_fiat": false,
-        "dev_buyer_fee_basis_points": 0,
-        "dev_seller_fee_basis_points": 0,
-        "only_proxied_transfers": false,
-        "opensea_buyer_fee_basis_points": 0,
-        "opensea_seller_fee_basis_points": 250,
-        "buyer_fee_basis_points": 0,
-        "seller_fee_basis_points": 250,
-        "payout_address": null
-    }
-]`
-
-const rawAssetContractsExpected = `[{
-        "address": "0x0e3a2a1f2146d86a604adc220b4967a898d7fe07",
-        "asset_contract_type": "non-fungible",
-        "created_date": "2019-11-01T06:39:04.363034",
-        "name": "Gods Unchained Cards",
-        "nft_version": "3.0",
-        "opensea_version": null,
-        "owner": 1691695,
-        "schema_name": "ERC721",
-        "symbol": "",
-        "total_supply": "1",
-        "description": "Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-        "external_link": "https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-        "image_url": "https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-        "default_to_fiat": false,
-        "dev_buyer_fee_basis_points": 0,
-        "dev_seller_fee_basis_points": 0,
-        "only_proxied_transfers": false,
-        "opensea_buyer_fee_basis_points": 0,
-        "opensea_seller_fee_basis_points": 250,
-        "buyer_fee_basis_points": 0,
-        "seller_fee_basis_points": 250,
-        "payout_address": null
-    },
-    {
-        "address": "0x564cb55c655f727b61d9baf258b547ca04e9e548",
-        "asset_contract_type": "non-fungible",
-        "created_date": "2019-10-29T12:28:37.643714",
-        "name": "Gods Unchained",
-        "nft_version": "3.0",
-        "opensea_version": null,
-        "owner": 1691695,
-        "schema_name": "ERC721",
-        "symbol": "",
-        "total_supply": "205",
-        "description": "Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-        "external_link": "https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-        "image_url": "https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-        "default_to_fiat": false,
-        "dev_buyer_fee_basis_points": 0,
-        "dev_seller_fee_basis_points": 0,
-        "only_proxied_transfers": false,
-        "opensea_buyer_fee_basis_points": 0,
-        "opensea_seller_fee_basis_points": 250,
-        "buyer_fee_basis_points": 0,
-        "seller_fee_basis_points": 250,
-        "payout_address": null
-    }
-]`
diff --git a/platform/ethereum/collection_v4_test.go b/platform/ethereum/collection_v4_test.go
deleted file mode 100644
index ed74216..0000000
--- a/platform/ethereum/collection_v4_test.go
+++ /dev/null
@@ -1,337 +0,0 @@
-package ethereum
-
-import (
-	"encoding/json"
-	"github.com/stretchr/testify/assert"
-	"github.com/trustwallet/blockatlas/coin"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"testing"
-)
-
-const collectionsOwnerV4 = "0x0875BCab22dE3d02402bc38aEe4104e1239374a7"
-
-const collectionsSrcV4 = `
-[
-   {
-      "primary_asset_contracts":[
-         {
-            "address":"0x06012c8cf97bead5deae237070f9587f8e7a266d",
-            "name":"CryptoKitties",
-            "symbol":"CKITTY",
-            "description":"CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
-            "external_link":"https://www.cryptokitties.co/",
-            "nft_version":"1.0",
-            "schema_name":"ERC721",
-            "display_data":{
-               "images":[
-                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/564155.svg",
-                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/546630.svg",
-                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/441529.svg",
-                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/552435.svg",
-                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/524748.png",
-                  "https://storage.googleapis.com/ck-kitty-image/0x06012c8cf97bead5deae237070f9587f8e7a266d/540800.svg"
-               ],
-               "card_display_style":"padded"
-            },
-            "image_url":"https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556588705.png"
-         }
-      ],
-      "name":"CryptoKitties",
-      "slug":"cryptokitties",
-      "image_url":"https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556588705.png",
-      "description":"CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
-      "external_url":"https://www.cryptokitties.co/",
-      "featured_image_url":"https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556589429.png",
-      "created_date":"2019-04-26T22:13:04.207050",
-      "owned_asset_count":3
-   },
-   {
-      "primary_asset_contracts":[
-         {
-            "address":"0xfaafdc07907ff5120a76b34b731b278c38d6043c",
-            "name":"Enjin",
-            "symbol":"",
-            "description":"",
-            "external_link":null,
-            "nft_version":null,
-            "schema_name":"ERC1155",
-            "display_data":{
-
-            },
-            "owner":null,
-            "created_date":"2019-08-02T23:43:14.666153",
-            "asset_contract_type":"semi-fungible"
-         }
-      ],
-      "name":"Age of Rust",
-      "slug":"age-of-rust",
-      "image_url":"https://storage.opensea.io/age-of-rust-1561960816.jpg",
-      "description":"Year 4424: The search begins for new life on the other side of the galaxy. Explore abandoned space stations, mysterious caverns, and ruins on far away worlds in order to unlock puzzles and secrets! Beware the rogue machines!",
-      "external_url":"https://www.ageofrust.games/",
-      "featured_image_url":null,
-      "created_date":"2019-09-03T02:35:56.063685",
-      "owned_asset_count":1
-   },
-   {
-      "primary_asset_contracts":[
-         {
-            "address":"0xee85966b4974d3c6f71a2779cc3b6f53afbc2b68",
-            "asset_contract_type":"fungible",
-            "created_date":"2019-10-16T07:36:16.102163",
-            "name":"Rare Chest",
-            "nft_version":null,
-            "opensea_version":null,
-            "owner":1610615,
-            "schema_name":"ERC20",
-            "symbol":"",
-            "total_supply":"1",
-            "description":"Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-            "external_link":"https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-            "image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-            "default_to_fiat":false,
-            "dev_buyer_fee_basis_points":0,
-            "dev_seller_fee_basis_points":0,
-            "only_proxied_transfers":false,
-            "opensea_buyer_fee_basis_points":0,
-            "opensea_seller_fee_basis_points":250,
-            "buyer_fee_basis_points":0,
-            "seller_fee_basis_points":250,
-            "payout_address":null
-         },
-         {
-            "address":"0x20d4cec36528e1c4563c1bfbe3de06aba70b22b4",
-            "asset_contract_type":"fungible",
-            "created_date":"2019-10-16T08:06:42.727997",
-            "name":"Legendary Chest",
-            "nft_version":null,
-            "opensea_version":null,
-            "owner":1610615,
-            "schema_name":"ERC20",
-            "symbol":"",
-            "total_supply":"1",
-            "description":"Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-            "external_link":"https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-            "image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-            "default_to_fiat":false,
-            "dev_buyer_fee_basis_points":0,
-            "dev_seller_fee_basis_points":0,
-            "only_proxied_transfers":false,
-            "opensea_buyer_fee_basis_points":0,
-            "opensea_seller_fee_basis_points":250,
-            "buyer_fee_basis_points":0,
-            "seller_fee_basis_points":250,
-            "payout_address":null
-         },
-         {
-            "address":"0x0e3a2a1f2146d86a604adc220b4967a898d7fe07",
-            "asset_contract_type":"non-fungible",
-            "created_date":"2019-11-01T06:39:04.363034",
-            "name":"Gods Unchained Cards",
-            "nft_version":"3.0",
-            "opensea_version":null,
-            "owner":1691695,
-            "schema_name":"ERC721",
-            "symbol":"",
-            "total_supply":"1",
-            "description":"Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-            "external_link":"https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-            "image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-            "default_to_fiat":false,
-            "dev_buyer_fee_basis_points":0,
-            "dev_seller_fee_basis_points":0,
-            "only_proxied_transfers":false,
-            "opensea_buyer_fee_basis_points":0,
-            "opensea_seller_fee_basis_points":250,
-            "buyer_fee_basis_points":0,
-            "seller_fee_basis_points":250,
-            "payout_address":null
-         },
-         {
-            "address":"0x564cb55c655f727b61d9baf258b547ca04e9e548",
-            "asset_contract_type":"non-fungible",
-            "created_date":"2019-10-29T12:28:37.643714",
-            "name":"Gods Unchained",
-            "nft_version":"3.0",
-            "opensea_version":null,
-            "owner":1691695,
-            "schema_name":"ERC721",
-            "symbol":"",
-            "total_supply":"205",
-            "description":"Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-            "external_link":"https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-            "image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-            "default_to_fiat":false,
-            "dev_buyer_fee_basis_points":0,
-            "dev_seller_fee_basis_points":0,
-            "only_proxied_transfers":false,
-            "opensea_buyer_fee_basis_points":0,
-            "opensea_seller_fee_basis_points":250,
-            "buyer_fee_basis_points":0,
-            "seller_fee_basis_points":250,
-            "payout_address":null
-         }
-      ],
-      "traits":{
-         "mana":{
-            "min":1,
-            "max":18
-         },
-         "health":{
-            "min":1,
-            "max":16
-         },
-         "attack":{
-            "min":1,
-            "max":16
-         }
-      },
-      "stats":{
-         "seven_day_volume":270.564697067863,
-         "seven_day_change":-0.014179906699531201,
-         "total_volume":11825.0384171324,
-         "count":6835331,
-         "num_owners":10791,
-         "market_cap":150892.14138332763,
-         "average_price":0.0396514694030496,
-         "items_sold":298233
-      },
-      "banner_image_url":null,
-      "chat_url":null,
-      "created_date":"2019-11-13T03:01:42.051246",
-      "default_to_fiat":false,
-      "description":"Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-      "dev_buyer_fee_basis_points":"0",
-      "dev_seller_fee_basis_points":"0",
-      "display_data":{
-         "images":[
-            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/25233.png",
-            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/152875.png",
-            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/25669.png",
-            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/9237.png",
-            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/9228.png",
-            "https://storage.googleapis.com/opensea-prod.appspot.com/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab/9231.png"
-         ],
-         "card_display_style":"contain"
-      },
-      "external_url":"https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-      "featured":true,
-      "featured_image_url":"https://storage.opensea.io/0x6ebeaf8e8e946f0716e6533a6f2cefc83f60e8ab-featured-1556589419.png",
-      "hidden":false,
-      "image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-      "is_subject_to_whitelist":false,
-      "large_image_url":"https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ",
-      "name":"Gods Unchained",
-      "only_proxied_transfers":false,
-      "opensea_buyer_fee_basis_points":"0",
-      "opensea_seller_fee_basis_points":"250",
-      "payout_address":null,
-      "require_email":false,
-      "short_description":null,
-      "slug":"gods-unchained",
-      "wiki_url":null,
-      "owned_asset_count":535
-   }
-]
-`
-
-var collection1DstV4 = blockatlas.Collection{
-	Name:         "CryptoKitties",
-	Symbol:       "",
-	Slug:         "cryptokitties",
-	ImageUrl:     "https://storage.opensea.io/0x06012c8cf97bead5deae237070f9587f8e7a266d-featured-1556588705.png",
-	Description:  "CryptoKitties is a game centered around breedable, collectible, and oh-so-adorable creatures we call CryptoKitties! Each cat is one-of-a-kind and 100% owned by you; it cannot be replicated, taken away, or destroyed.",
-	ExternalLink: "https://www.cryptokitties.co/",
-	Total:        3,
-	Id:           "cryptokitties",
-	Address:      "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
-	Coin:         60,
-}
-
-var collection2DstV4 = blockatlas.Collection{
-	Name:         "Age of Rust",
-	Symbol:       "",
-	Slug:         "age-of-rust",
-	ImageUrl:     "https://storage.opensea.io/age-of-rust-1561960816.jpg",
-	Description:  "Year 4424: The search begins for new life on the other side of the galaxy. Explore abandoned space stations, mysterious caverns, and ruins on far away worlds in order to unlock puzzles and secrets! Beware the rogue machines!",
-	ExternalLink: "https://www.ageofrust.games/",
-	Total:        1,
-	Id:           "age-of-rust",
-	Address:      "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
-	Coin:         60,
-}
-
-var collection3DstV4 = blockatlas.Collection{
-	Name:         "Gods Unchained",
-	Symbol:       "",
-	Slug:         "gods-unchained",
-	ImageUrl:     "https://lh3.googleusercontent.com/yArciVdcDv3O2R-O8XCxx3YEYZdzpiCMdossjUgv0kpLIluUQ1bYN_dyEk5xcvBEOgeq0zNIoWOh7TL9DvUEv--OLQ=s60",
-	Description:  "Gods Unchained is a free-to-play, turn-based competitive trading card game in which cards can be bought and sold on the OpenSea marketplace. Players use their collection to build decks of cards, and select a God to play with at the start of each match. The goal of the game is to reduce your opponent's life to zero. Each deck contains exactly 30 cards. On OpenSea, cards can be sold for a fixed price, auctioned, or sold in bundles.",
-	ExternalLink: "https://godsunchained.com/?refcode=0x5b3256965e7C3cF26E11FCAf296DfC8807C01073",
-	Total:        535,
-	Id:           "gods-unchained",
-	Address:      "0x0875BCab22dE3d02402bc38aEe4104e1239374a7",
-	Coin:         60,
-}
-
-func TestNormalizeCollectionV4(t *testing.T) {
-	var collections []Collection
-	err := json.Unmarshal([]byte(collectionsSrcV4), &collections)
-	assert.Nil(t, err)
-	page := NormalizeCollectionsV4(collections, coin.ETH, collectionsOwnerV4)
-	assert.Equal(t, 3, len(page), "collections could not be normalized")
-	expected := blockatlas.CollectionPage{collection1DstV4, collection2DstV4, collection3DstV4}
-	assert.Equal(t, page, expected, "collections don't equal")
-}
-
-const collectibleSrcV4 = `
-[
-  {
-    "token_id": "54277541829991970107421667568590323026590803461896874578610080514640537714688",
-    "image_url": "https://storage.opensea.io/0xfaafdc07907ff5120a76b34b731b278c38d6043c/54277541829991970107421667568590323026590803461896874578610080514640537714688-1564858806.png",
-    "image_preview_url": "https://storage.opensea.io/0xfaafdc07907ff5120a76b34b731b278c38d6043c-preview/54277541829991970107421667568590323026590803461896874578610080514640537714688-1564858807.png",
-    "name": "Rustbits",
-    "description": "Rustbits are the main token of use within the Age of Rust game universe. You need Rustbits to not only play Age of Rust, but also to purchase in-game cryptoitems as well. Rustbits are radioactive rust scraped off of hulls of abandoned ships that are in orbit around a hidden planet, which is also a gas giant. The planet is so radioactive, it damages ships and kills anyone that gets close to it. Getting bits of rust off of ships is highly rare and prized.",
-    "external_link": "",
-    "asset_contract": {
-      "address": "0xfaafdc07907ff5120a76b34b731b278c38d6043c",
-      "name": "Enjin",
-      "external_link": null,
-      "nft_version": null,
-      "schema_name": "ERC1155",
-	  "display_data": {}
-    },
-	"collection": {
-		"slug": "age-of-rust",
-        "name": "Age of Rust",
-        "external_url": "https://opensea.io/"
-    },
-    "permalink": "https://opensea.io/assets/0xfaafdc07907ff5120a76b34b731b278c38d6043c/54277541829991970107421667568590323026590803461896874578610080514640537714688"
-  }
-]
-`
-
-var collectibleDstV4 = blockatlas.Collectible{
-	ID:               "0xfaafdc07907ff5120a76b34b731b278c38d6043c-54277541829991970107421667568590323026590803461896874578610080514640537714688",
-	CollectionID:     "age-of-rust",
-	TokenID:          "54277541829991970107421667568590323026590803461896874578610080514640537714688",
-	CategoryContract: "",
-	ContractAddress:  "0xfaafdc07907ff5120a76b34b731b278c38d6043c",
-	Category:         "Age of Rust",
-	ImageUrl:         "https://storage.opensea.io/0xfaafdc07907ff5120a76b34b731b278c38d6043c-preview/54277541829991970107421667568590323026590803461896874578610080514640537714688-1564858807.png",
-	ExternalLink:     "https://opensea.io/",
-	ProviderLink:     "https://opensea.io/assets/0xfaafdc07907ff5120a76b34b731b278c38d6043c/54277541829991970107421667568590323026590803461896874578610080514640537714688",
-	Type:             "ERC1155",
-	Description:      "Rustbits are the main token of use within the Age of Rust game universe. You need Rustbits to not only play Age of Rust, but also to purchase in-game cryptoitems as well. Rustbits are radioactive rust scraped off of hulls of abandoned ships that are in orbit around a hidden planet, which is also a gas giant. The planet is so radioactive, it damages ships and kills anyone that gets close to it. Getting bits of rust off of ships is highly rare and prized.",
-	Coin:             60,
-	Name:             "Rustbits",
-}
-
-func TestNormalizeCollectibleV4(t *testing.T) {
-	var collectibles []Collectible
-	err := json.Unmarshal([]byte(collectibleSrcV4), &collectibles)
-	assert.Nil(t, err)
-	page := NormalizeCollectiblePageV4(collectibles, coin.ETH)
-	assert.Equal(t, len(page), 1, "collectible could not be normalized")
-	expected := blockatlas.CollectiblePage{collectibleDstV4}
-	assert.Equal(t, page, expected, "collectible don't equal")
-}
diff --git a/tests/postman/Blockatlas.postman_collection.json b/tests/postman/Blockatlas.postman_collection.json
index 4386043..884eb88 100644
--- a/tests/postman/Blockatlas.postman_collection.json
+++ b/tests/postman/Blockatlas.postman_collection.json
@@ -1,6 +1,6 @@
 {
 	"info": {
-		"_postman_id": "60e7e382-2f40-487f-a291-3e62daadadd8",
+		"_postman_id": "7c16faaa-629e-4a75-a8bc-2ccaacb540ff",
 		"name": "Blockatlas",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
 	},
@@ -984,7 +984,7 @@
 					"name": "collection",
 					"item": [
 						{
-							"name": "collection",
+							"name": "collection v4",
 							"event": [
 								{
 									"listen": "prerequest",
@@ -1025,9 +1025,6 @@
 											"          \"token_id\": {",
 											"            \"type\": \"string\"",
 											"          },",
-											"          \"category_contract\": {",
-											"            \"type\": \"string\"",
-											"          },",
 											"          \"contract_address\": {",
 											"            \"type\": \"string\"",
 											"          },",
@@ -1054,134 +1051,8 @@
 											"          },",
 											"          \"name\": {",
 											"            \"type\": \"string\"",
-											"          }",
-											"        }",
-											"      }",
-											"    },",
-											"    \"status\": {",
-											"      \"type\": \"boolean\"",
-											"    }",
-											"  }",
-											"};",
-											"",
-											"let handler = pm.variables.get(\"handler\");",
-											"let address = pm.variables.get(\"address\");",
-											"var jsonData = pm.response.json();",
-											"",
-											"pm.test(handler + \" - response must be valid and have a body: \" + address, function () {",
-											"    pm.response.to.have.status(200);",
-											"    pm.response.to.be.ok;",
-											"    pm.response.to.not.be.error;",
-											"    pm.response.to.be.withBody;",
-											"    pm.response.to.be.json;",
-											"});",
-											"",
-											"pm.test(handler + \" - schema is valid: \" + address, function() {",
-											"    pm.expect(ajv.validate(schema, jsonData)).to.be.true;",
-											"    console.log(JSON.stringify(jsonData))",
-											"});",
-											""
-										],
-										"type": "text/javascript"
-									}
-								}
-							],
-							"request": {
-								"method": "GET",
-								"header": [
-									{
-										"key": "Authorization",
-										"value": "Bearer {{platform_auth}}",
-										"type": "text"
-									}
-								],
-								"url": {
-									"raw": "{{host}}/v2/{{handler}}/collections/{{address}}/collection/{{collection}}",
-									"host": [
-										"{{host}}"
-									],
-									"path": [
-										"v2",
-										"{{handler}}",
-										"collections",
-										"{{address}}",
-										"collection",
-										"{{collection}}"
-									]
-								}
-							},
-							"response": []
-						},
-						{
-							"name": "collection v4",
-							"event": [
-								{
-									"listen": "prerequest",
-									"script": {
-										"id": "7312c9e7-5330-421a-9ca8-171101050e38",
-										"exec": [
-											""
-										],
-										"type": "text/javascript"
-									}
-								},
-								{
-									"listen": "test",
-									"script": {
-										"id": "acec8baa-0ef0-4703-ab83-c555260deffa",
-										"exec": [
-											"var Ajv = require('ajv');",
-											"var ajv = new Ajv({logger: console});",
-											"let schema = {",
-											"  \"type\": \"object\",",
-											"  \"properties\": {",
-											"    \"total\": {",
-											"      \"type\": \"integer\"",
-											"    },",
-											"    \"docs\": {",
-											"      \"type\": \"array\",",
-											"      \"minItems\": 1,",
-											"      \"uniqueItems\": true,",
-											"      \"items\": {",
-											"        \"type\": \"object\",",
-											"        \"properties\": {",
-											"          \"id\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"collection_id\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"token_id\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"category_contract\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"contract_address\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"category\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"image_url\": {",
-											"            \"type\": \"string\"",
 											"          },",
-											"          \"external_link\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"provider_link\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"type\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"description\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"coin\": {",
-											"            \"type\": \"integer\"",
-											"          },",
-											"          \"name\": {",
+											"          \"nft_version\": {",
 											"            \"type\": \"string\"",
 											"          }",
 											"        }",
@@ -1225,12 +1096,12 @@
 									}
 								],
 								"url": {
-									"raw": "{{host}}/v2/{{handler}}/collections/{{address}}/collection/{{collection}}",
+									"raw": "{{host}}/v4/{{handler}}/collections/{{address}}/collection/{{collection}}",
 									"host": [
 										"{{host}}"
 									],
 									"path": [
-										"v2",
+										"v4",
 										"{{handler}}",
 										"collections",
 										"{{address}}",
@@ -1242,7 +1113,7 @@
 							"response": []
 						},
 						{
-							"name": "collections",
+							"name": "collections batch v4",
 							"event": [
 								{
 									"listen": "prerequest",
@@ -1280,12 +1151,6 @@
 											"          \"name\": {",
 											"            \"type\": \"string\"",
 											"          },",
-											"          \"symbol\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"slug\": {",
-											"            \"type\": \"string\"",
-											"          },",
 											"          \"image_url\": {",
 											"            \"type\": \"string\"",
 											"          },",
@@ -1295,415 +1160,9 @@
 											"          \"total\": {",
 											"            \"type\": \"integer\"",
 											"          },",
-											"          \"category_address\": {",
-											"            \"type\": \"string\"",
-											"          },",
 											"          \"address\": {",
 											"            \"type\": \"string\"",
 											"          },",
-											"          \"nft_version\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"coin\": {",
-											"            \"type\": \"integer\"",
-											"          },",
-											"          \"type\": {",
-											"            \"type\": \"string\"",
-											"          }",
-											"        }",
-											"      }",
-											"    },",
-											"    \"status\": {",
-											"      \"type\": \"boolean\"",
-											"    }",
-											"  }",
-											"};",
-											"",
-											"let handler = pm.variables.get(\"handler\");",
-											"let address = pm.variables.get(\"address\");",
-											"var jsonData = pm.response.json();",
-											"",
-											"pm.test(handler + \" - response must be valid and have a body: \" + address, function () {",
-											"    pm.response.to.have.status(200);",
-											"    pm.response.to.be.ok;",
-											"    pm.response.to.not.be.error;",
-											"    pm.response.to.be.withBody;",
-											"    pm.response.to.be.json;",
-											"});",
-											"",
-											"pm.test(handler + \" - schema is valid: \" + address, function() {",
-											"    pm.expect(ajv.validate(schema, jsonData)).to.be.true;",
-											"    console.log(JSON.stringify(jsonData))",
-											"});",
-											""
-										],
-										"type": "text/javascript"
-									}
-								}
-							],
-							"request": {
-								"method": "GET",
-								"header": [
-									{
-										"key": "Authorization",
-										"value": "Bearer {{platform_auth}}",
-										"type": "text"
-									}
-								],
-								"url": {
-									"raw": "{{host}}/v2/{{handler}}/collections/{{address}}",
-									"host": [
-										"{{host}}"
-									],
-									"path": [
-										"v2",
-										"{{handler}}",
-										"collections",
-										"{{address}}"
-									]
-								}
-							},
-							"response": []
-						},
-						{
-							"name": "collections batch",
-							"event": [
-								{
-									"listen": "prerequest",
-									"script": {
-										"id": "7312c9e7-5330-421a-9ca8-171101050e38",
-										"exec": [
-											""
-										],
-										"type": "text/javascript"
-									}
-								},
-								{
-									"listen": "test",
-									"script": {
-										"id": "acec8baa-0ef0-4703-ab83-c555260deffa",
-										"exec": [
-											"var Ajv = require('ajv');",
-											"var ajv = new Ajv({logger: console});",
-											"let schema = {",
-											"  \"type\": \"object\",",
-											"  \"properties\": {",
-											"    \"total\": {",
-											"      \"type\": \"integer\"",
-											"    },",
-											"    \"docs\": {",
-											"      \"type\": \"array\",",
-											"      \"minItems\": 1,",
-											"      \"uniqueItems\": true,",
-											"      \"items\": {",
-											"        \"type\": \"object\",",
-											"        \"properties\": {",
-											"          \"id\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"name\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"symbol\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"slug\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"image_url\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"external_link\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"total\": {",
-											"            \"type\": \"integer\"",
-											"          },",
-											"          \"category_address\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"address\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"nft_version\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"coin\": {",
-											"            \"type\": \"integer\"",
-											"          },",
-											"          \"type\": {",
-											"            \"type\": \"string\"",
-											"          }",
-											"        }",
-											"      }",
-											"    },",
-											"    \"status\": {",
-											"      \"type\": \"boolean\"",
-											"    }",
-											"  }",
-											"};",
-											"",
-											"let handler = pm.variables.get(\"handler\");",
-											"let address = pm.variables.get(\"address\");",
-											"var jsonData = pm.response.json();",
-											"",
-											"pm.test(handler + \" - response must be valid and have a body: \" + address, function () {",
-											"    pm.response.to.have.status(200);",
-											"    pm.response.to.be.ok;",
-											"    pm.response.to.not.be.error;",
-											"    pm.response.to.be.withBody;",
-											"    pm.response.to.be.json;",
-											"});",
-											"",
-											"pm.test(handler + \" - schema is valid: \" + address, function() {",
-											"    pm.expect(ajv.validate(schema, jsonData)).to.be.true;",
-											"    console.log(JSON.stringify(jsonData))",
-											"});",
-											""
-										],
-										"type": "text/javascript"
-									}
-								}
-							],
-							"request": {
-								"method": "POST",
-								"header": [
-									{
-										"key": "Authorization",
-										"value": "Bearer {{platform_auth}}",
-										"type": "text"
-									},
-									{
-										"key": "Content-Type",
-										"name": "Content-Type",
-										"value": "application/json",
-										"type": "text"
-									}
-								],
-								"body": {
-									"mode": "raw",
-									"raw": "{\n\t\"{{coin}}\": [\"{{address}}\"]\n}",
-									"options": {
-										"raw": {
-											"language": "json"
-										}
-									}
-								},
-								"url": {
-									"raw": "{{host}}/v2/collectibles/categories",
-									"host": [
-										"{{host}}"
-									],
-									"path": [
-										"v2",
-										"collectibles",
-										"categories"
-									]
-								}
-							},
-							"response": []
-						},
-						{
-							"name": "collections batch v3 ",
-							"event": [
-								{
-									"listen": "prerequest",
-									"script": {
-										"id": "7312c9e7-5330-421a-9ca8-171101050e38",
-										"exec": [
-											""
-										],
-										"type": "text/javascript"
-									}
-								},
-								{
-									"listen": "test",
-									"script": {
-										"id": "acec8baa-0ef0-4703-ab83-c555260deffa",
-										"exec": [
-											"var Ajv = require('ajv');",
-											"var ajv = new Ajv({logger: console});",
-											"let schema = {",
-											"  \"type\": \"object\",",
-											"  \"properties\": {",
-											"    \"total\": {",
-											"      \"type\": \"integer\"",
-											"    },",
-											"    \"docs\": {",
-											"      \"type\": \"array\",",
-											"      \"minItems\": 1,",
-											"      \"uniqueItems\": true,",
-											"      \"items\": {",
-											"        \"type\": \"object\",",
-											"        \"properties\": {",
-											"          \"id\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"name\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"symbol\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"slug\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"image_url\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"external_link\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"total\": {",
-											"            \"type\": \"integer\"",
-											"          },",
-											"          \"category_address\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"address\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"nft_version\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"coin\": {",
-											"            \"type\": \"integer\"",
-											"          },",
-											"          \"type\": {",
-											"            \"type\": \"string\"",
-											"          }",
-											"        }",
-											"      }",
-											"    },",
-											"    \"status\": {",
-											"      \"type\": \"boolean\"",
-											"    }",
-											"  }",
-											"};",
-											"",
-											"let handler = pm.variables.get(\"handler\");",
-											"let address = pm.variables.get(\"address\");",
-											"var jsonData = pm.response.json();",
-											"",
-											"pm.test(handler + \" - response must be valid and have a body: \" + address, function () {",
-											"    pm.response.to.have.status(200);",
-											"    pm.response.to.be.ok;",
-											"    pm.response.to.not.be.error;",
-											"    pm.response.to.be.withBody;",
-											"    pm.response.to.be.json;",
-											"});",
-											"",
-											"pm.test(handler + \" - schema is valid: \" + address, function() {",
-											"    pm.expect(ajv.validate(schema, jsonData)).to.be.true;",
-											"    console.log(JSON.stringify(jsonData))",
-											"});",
-											""
-										],
-										"type": "text/javascript"
-									}
-								}
-							],
-							"request": {
-								"method": "POST",
-								"header": [
-									{
-										"key": "Authorization",
-										"value": "Bearer {{platform_auth}}",
-										"type": "text"
-									},
-									{
-										"key": "Content-Type",
-										"name": "Content-Type",
-										"value": "application/json",
-										"type": "text"
-									}
-								],
-								"body": {
-									"mode": "raw",
-									"raw": "{\n\t\"{{coin}}\": [\"{{address}}\"]\n}",
-									"options": {
-										"raw": {
-											"language": "json"
-										}
-									}
-								},
-								"url": {
-									"raw": "{{host}}/v3/collectibles/categories",
-									"host": [
-										"{{host}}"
-									],
-									"path": [
-										"v3",
-										"collectibles",
-										"categories"
-									]
-								}
-							},
-							"response": []
-						},
-						{
-							"name": "collections batch v4",
-							"event": [
-								{
-									"listen": "prerequest",
-									"script": {
-										"id": "7312c9e7-5330-421a-9ca8-171101050e38",
-										"exec": [
-											""
-										],
-										"type": "text/javascript"
-									}
-								},
-								{
-									"listen": "test",
-									"script": {
-										"id": "acec8baa-0ef0-4703-ab83-c555260deffa",
-										"exec": [
-											"var Ajv = require('ajv');",
-											"var ajv = new Ajv({logger: console});",
-											"let schema = {",
-											"  \"type\": \"object\",",
-											"  \"properties\": {",
-											"    \"total\": {",
-											"      \"type\": \"integer\"",
-											"    },",
-											"    \"docs\": {",
-											"      \"type\": \"array\",",
-											"      \"minItems\": 1,",
-											"      \"uniqueItems\": true,",
-											"      \"items\": {",
-											"        \"type\": \"object\",",
-											"        \"properties\": {",
-											"          \"id\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"name\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"symbol\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"slug\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"image_url\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"external_link\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"total\": {",
-											"            \"type\": \"integer\"",
-											"          },",
-											"          \"category_address\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"address\": {",
-											"            \"type\": \"string\"",
-											"          },",
-											"          \"nft_version\": {",
-											"            \"type\": \"string\"",
-											"          },",
 											"          \"coin\": {",
 											"            \"type\": \"integer\"",
 											"          },",
@@ -1766,12 +1225,12 @@
 									}
 								},
 								"url": {
-									"raw": "{{host}}/v3/collectibles/categories",
+									"raw": "{{host}}/v4/collectibles/categories",
 									"host": [
 										"{{host}}"
 									],
 									"path": [
-										"v3",
+										"v4",
 										"collectibles",
 										"categories"
 									]
@@ -2587,29 +2046,29 @@
 	],
 	"variable": [
 		{
-			"id": "ad2326b7-fb90-4b11-a1e3-dfe8ea3aae65",
+			"id": "c6d3b134-d68f-4747-b5c5-a687412f2166",
 			"key": "host",
 			"value": "http://localhost:8420",
 			"type": "string"
 		},
 		{
-			"id": "7ae1014f-44e1-4c16-aacd-2be971ee15ef",
+			"id": "cfde5bdb-eecf-472c-b1bf-d944a63befff",
 			"key": "observer_auth",
 			"value": "test",
 			"type": "string"
 		},
 		{
-			"id": "0d3dcacb-5b63-409a-8150-9e06d24e1eb4",
+			"id": "1455449b-6372-4c16-81d9-ed93ab4aafd7",
 			"key": "market_auth",
 			"value": "",
 			"type": "string"
 		},
 		{
-			"id": "c42f8723-ad35-45ea-9dd2-066ced0780bc",
+			"id": "4180368d-8afe-4432-8ecb-48db2f778a72",
 			"key": "platform_auth",
 			"value": "",
 			"type": "string"
 		}
 	],
 	"protocolProfileBehavior": {}
-}
+}
\ No newline at end of file
diff --git a/tests/postman/collection_data.json b/tests/postman/collection_data.json
index 0213e2d..4e09b01 100644
--- a/tests/postman/collection_data.json
+++ b/tests/postman/collection_data.json
@@ -2,7 +2,7 @@
     {
         "coin": 60,
         "handler": "ethereum",
-        "address": "0x7d8bf18C7cE84b3E175b339c4Ca93aEd1dD166F1",
-        "collection": "0x35b7838dd7507ada69610397a85310ae0abd5034"
+        "address": "0x84E79D544B4b13bC3560069cfD56A9D5bbE7521d",
+        "collection": "unstoppable-domains"
     }
 ]