Repository: trustwallet/blockatlas
Pull request #916 (2020-02-23 19:34:52)

Refactor and fixes (Split api routes, remove metrics and build packages, change nano and config.yml)
Refactor api, remove useless and unused packages, remove sentry calls, remove metrics, remove build package, change postman and small code fixes, fix critical bug with AUTH metrics.token at observer api

todo[bot]: ## remove once most of the clients will be updated (deadline: March 17th)

https://github.com/trustwallet/blockatlas/blob/ba69d0804a57a828ed5c7bb3e8c02d0fa06425c4/api/collection.go#L144-L149

---

###### This comment was generated by [todo](https://todo.jasonet.co) based on a `TODO` comment in ba69d0804a57a828ed5c7bb3e8c02d0fa06425c4 in #916. cc @trustwallet.

Patch:
diff --git a/Makefile b/Makefile
index 8c9bbd5..49451de 100644
--- a/Makefile
+++ b/Makefile
@@ -154,7 +154,7 @@ ifeq (,$(test))
 	@bash -c "$(MAKE) newman test=healthcheck host=$(host)"
 	@bash -c "$(MAKE) newman test=observer host=$(host)"
 else
-	@newman run pkg/tests/postman/Blockatlas.postman_collection.json --folder $(test) -d pkg/tests/postman/$(test)_data.json --env-var "host=$(host)"
+	@newman run tests/postman/Blockatlas.postman_collection.json --folder $(test) -d tests/postman/$(test)_data.json --env-var "host=$(host)"
 endif
 
 go-compile: go-get go-build
@@ -190,11 +190,11 @@ go-test:
 
 go-functional:
 	@echo "  >  Running functional tests"
-	GOBIN=$(GOBIN) TEST_CONFIG=$(CONFIG_FILE) go test -race -tags=functional -v ./pkg/tests/functional
+	GOBIN=$(GOBIN) TEST_CONFIG=$(CONFIG_FILE) go test -race -tags=functional -v ./tests/functional
 
 go-integration:
 	@echo "  >  Running integration tests"
-	GOBIN=$(GOBIN) TEST_CONFIG=$(CONFIG_FILE) go test -race -tags=integration -v ./pkg/tests/integration
+	GOBIN=$(GOBIN) TEST_CONFIG=$(CONFIG_FILE) go test -race -tags=integration -v ./tests/integration
 
 go-fmt:
 	@echo "  >  Format all go files"
diff --git a/README.md b/README.md
index de5746f..1a1eab1 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ The observer API watches the chain for new transactions and generates notificati
 ### Requirements
 
  * [Go Toolchain](https://golang.org/doc/install) versions 1.13+
- * [Redis](https://redis.io/topics/quickstart) instance (observer and markets)
+ * [Redis](https://redis.io/topics/quickstart) instance for platform_observer
 
 ### From Source
 
diff --git a/api/batch.go b/api/batch.go
deleted file mode 100644
index 9f511c8..0000000
--- a/api/batch.go
+++ /dev/null
@@ -1,179 +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/ginutils"
-	"github.com/trustwallet/blockatlas/pkg/ginutils/gincache"
-	"github.com/trustwallet/blockatlas/platform"
-	"strconv"
-	"time"
-)
-
-type AddressBatchRequest struct {
-	Address string `json:"address"`
-	CoinBatchRequest
-}
-
-type CoinBatchRequest struct {
-	Coin uint `json:"coin"`
-}
-
-type ENSBatchRequest struct {
-	Coins []uint64 `json:"coins"`
-	Name  string   `json:"name"`
-}
-
-type AddressesRequest []AddressBatchRequest
-type 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})
-	}))
-}
-
-// @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)
-	})
-}
diff --git a/api/handlers.go b/api/collection.go
similarity index 54%
rename from api/handlers.go
rename to api/collection.go
index 322b218..35c57e0 100644
--- a/api/handlers.go
+++ b/api/collection.go
@@ -1,113 +1,13 @@
 package api
 
 import (
-	"github.com/chenjiandongx/ginprom"
 	"github.com/gin-gonic/gin"
-	"github.com/prometheus/client_golang/prometheus/promhttp"
-	"github.com/spf13/viper"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
 	"github.com/trustwallet/blockatlas/pkg/ginutils"
-	"github.com/trustwallet/blockatlas/pkg/metrics"
-	"net/http"
-	"sort"
+	"github.com/trustwallet/blockatlas/platform"
+	"strconv"
 )
 
-// @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 Collections
 // @ID collections_v2
 // @Description Get all collections from the address
@@ -232,49 +132,85 @@ func makeCollectionRoute(router gin.IRouter, api blockatlas.Platform) {
 	})
 }
 
-// @Summary Get Tokens
-// @ID tokens
-// @Description Get tokens from the address
+// @Description Get collection categories
+// @ID collection_categories_v2
+// @Summary Get list of collections from a specific coin and addresses
 // @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)
+// @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
 		}
 
-		tl, err := tokenAPI.GetTokenListByAddress(address)
-		if err != nil {
+		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
 		}
 
-		ginutils.RenderSuccess(c, blockatlas.DocsResponse{Docs: tl})
+		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)
 	})
 }
 
-func MakeMetricsRoute(router gin.IRouter) {
-	router.Use(metrics.PromMiddleware())
-	m := router.Group("/metrics")
-	m.Use(ginutils.TokenAuthMiddleware(viper.GetString("metrics.api_token")))
-	m.GET("/", ginprom.PromHandler(promhttp.Handler()))
-}
-
 func emptyPage(c *gin.Context) {
 	var page blockatlas.TxPage
 	ginutils.RenderSuccess(c, &page)
diff --git a/api/naming_service.go b/api/domain.go
similarity index 100%
rename from api/naming_service.go
rename to api/domain.go
diff --git a/api/meta.go b/api/meta.go
deleted file mode 100644
index 2d9a678..0000000
--- a/api/meta.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package api
-
-import (
-	"github.com/gin-gonic/gin"
-	"github.com/trustwallet/blockatlas/pkg/ginutils"
-)
-
-func GetRoot(c *gin.Context) {
-	ginutils.RenderSuccess(c,
-		`Welcome to the Block Atlas API!
-
-Don't know how you landed here?
-Visit https://trustwallet.com to get back to the main page.
-
-If you know what you're doing:
- - Visit /v1/ to list platforms
- - Source: https://github.com/trustwallet/blockatlas
- - Any questions? https://t.me/walletcore
-`)
-}
-
-func getEnabledEndpoints(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)
-}
diff --git a/api/observer.go b/api/observer.go
index fc230b6..160832f 100644
--- a/api/observer.go
+++ b/api/observer.go
@@ -2,20 +2,12 @@ package api
 
 import (
 	"github.com/gin-gonic/gin"
-	"github.com/spf13/viper"
 	"github.com/trustwallet/blockatlas/pkg/blockatlas"
 	"github.com/trustwallet/blockatlas/pkg/ginutils"
 	"github.com/trustwallet/blockatlas/platform"
 	"github.com/trustwallet/blockatlas/storage"
 )
 
-func SetupObserverAPI(router gin.IRouter, db *storage.Storage) {
-	router.Use(ginutils.TokenAuthMiddleware(viper.GetString("metrics.api_token")))
-	router.POST("/webhook/register", addCall(db))
-	router.DELETE("/webhook/register", deleteCall(db))
-	router.GET("/status", statusCall(db))
-}
-
 // @Summary Create a webhook
 // @ID create_webhook
 // @Description Create a webhook for addresses transactions
diff --git a/api/routes.go b/api/routes.go
index 9105d6d..b4e8a9a 100644
--- a/api/routes.go
+++ b/api/routes.go
@@ -3,17 +3,40 @@ package api
 import (
 	"fmt"
 	"github.com/gin-gonic/gin"
+	"github.com/spf13/viper"
+	"github.com/trustwallet/blockatlas/internal"
+	"github.com/trustwallet/blockatlas/pkg/ginutils"
 	"github.com/trustwallet/blockatlas/pkg/logger"
 	"github.com/trustwallet/blockatlas/platform"
+	"github.com/trustwallet/blockatlas/storage"
 )
 
 var routers = make(map[string]gin.IRouter)
 
-func LoadPlatforms(root gin.IRouter) {
+func SetupObserverAPI(router gin.IRouter, db *storage.Storage) {
+	router.GET("/", GetRoot)
+	router.GET("/status", GetStatus)
+
+	observerAPI := router.Group("/observer/v1")
+	observerAPI.Use(ginutils.TokenAuthMiddleware(viper.GetString("observer.auth")))
+	observerAPI.POST("/webhook/register", addCall(db))
+	observerAPI.DELETE("/webhook/register", deleteCall(db))
+	observerAPI.GET("/status", statusCall(db))
+
+	logger.Info("Routes set up", logger.Params{"routes": len(routers)})
+}
+
+func SetupPlatformAPI(root gin.IRouter) {
+
+	root.GET("/", GetRoot)
+	root.GET("/status", GetStatus)
+
 	v1 := root.Group("/v1")
 	v2 := root.Group("/v2")
 	v3 := root.Group("/v3")
 
+	v1.GET("/", GetSupportedEndpoints)
+
 	for _, txAPI := range platform.Platforms {
 		router := getRouter(v1, txAPI.Coin().Handle)
 		makeTxRouteV1(router, txAPI)
@@ -34,15 +57,14 @@ func LoadPlatforms(root gin.IRouter) {
 	}
 
 	for _, collectionAPI := range platform.Platforms {
-		routerv2 := getRouter(v2, collectionAPI.Coin().Handle)
-		routerv3 := getRouter(v3, collectionAPI.Coin().Handle)
+		routerV2 := getRouter(v2, collectionAPI.Coin().Handle)
+		routerV3 := getRouter(v3, collectionAPI.Coin().Handle)
 
-		makeCollectionsRoute(routerv3, collectionAPI)
-		makeCollectionRoute(routerv3, collectionAPI)
+		makeCollectionsRoute(routerV3, collectionAPI)
+		makeCollectionRoute(routerV3, collectionAPI)
 
-		//TODO: remove once most of the clients will be updated (deadline: March 17th)
-		oldMakeCollectionRoute(routerv2, collectionAPI)
-		oldMakeCollectionsRoute(routerv2, collectionAPI)
+		oldMakeCollectionRoute(routerV2, collectionAPI)
+		oldMakeCollectionsRoute(routerV2, collectionAPI)
 	}
 
 	for _, customAPI := range platform.CustomAPIs {
@@ -50,21 +72,17 @@ func LoadPlatforms(root gin.IRouter) {
 		customAPI.RegisterRoutes(router)
 	}
 
-	{
-		ns := root.Group("/ns")
-		batchNs := v2.Group("/ns")
-		MakeLookupRoute(ns)
-		MakeLookupBatchRoute(batchNs)
-	}
+	ns := root.Group("/ns")
+	batchNs := v2.Group("/ns")
+	MakeLookupRoute(ns)
+	MakeLookupBatchRoute(batchNs)
 
-	//TODO: remove once most of the clients will be updated (deadline: March 17th)
 	oldMakeCategoriesBatchRoute(v2)
 	makeCategoriesBatchRoute(v3)
 	makeStakingDelegationsBatchRoute(v2)
 	makeStakingDelegationsSimpleBatchRoute(v2)
 
 	logger.Info("Routes set up", logger.Params{"routes": len(routers)})
-	v1.GET("/", getEnabledEndpoints)
 }
 
 // getRouter lazy loads routers
@@ -80,3 +98,23 @@ func getRouter(router *gin.RouterGroup, handle string) gin.IRouter {
 		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/stake.go b/api/staking.go
similarity index 55%
rename from api/stake.go
rename to api/staking.go
index f691669..881d26b 100644
--- a/api/stake.go
+++ b/api/staking.go
@@ -2,15 +2,107 @@ 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
diff --git a/api/transaction.go b/api/transaction.go
new file mode 100644
index 0000000..002943d
--- /dev/null
+++ b/api/transaction.go
@@ -0,0 +1,141 @@
+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/build/build.go b/build/build.go
deleted file mode 100644
index 49f78ec..0000000
--- a/build/build.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package build
-
-import (
-	"fmt"
-	"runtime"
-)
-
-var (
-	Version = "dev"
-	Build   = "dev"
-	Date    = ""
-)
-
-func LogVersionInfo() {
-	fmt.Printf(`
-************************************************
-Version: %v
-Build: %v
-Date: %v
-OS: %s
-Arch: %s
-Go: %s
-************************************************
-`,
-		Version, Build, Date, runtime.GOOS, runtime.GOARCH, runtime.Version())
-}
diff --git a/cmd/observer_api/main.go b/cmd/observer_api/main.go
index 0eabaa9..ce72ea5 100644
--- a/cmd/observer_api/main.go
+++ b/cmd/observer_api/main.go
@@ -4,11 +4,9 @@ import (
 	"github.com/gin-gonic/gin"
 	"github.com/spf13/viper"
 	"github.com/trustwallet/blockatlas/api"
-	"github.com/trustwallet/blockatlas/build"
 	_ "github.com/trustwallet/blockatlas/docs"
 	"github.com/trustwallet/blockatlas/internal"
 	"github.com/trustwallet/blockatlas/pkg/ginutils"
-	"github.com/trustwallet/blockatlas/pkg/logger"
 	"github.com/trustwallet/blockatlas/storage"
 )
 
@@ -24,7 +22,6 @@ var (
 )
 
 func init() {
-	build.LogVersionInfo()
 	port, confPath, sg, cache = internal.InitAPIWithRedis(defaultPort, defaultConfigPath)
 }
 
@@ -35,22 +32,8 @@ func main() {
 	engine.Use(ginutils.CheckReverseProxy, *sg)
 	engine.Use(ginutils.CORSMiddleware())
 	engine.Use(gin.Logger())
-
 	engine.OPTIONS("/*path", ginutils.CORSMiddleware())
-	engine.GET("/", api.GetRoot)
-	engine.GET("/status", func(c *gin.Context) {
-		ginutils.RenderSuccess(c, map[string]interface{}{
-			"status":  true,
-			"version": build.Version,
-			"build":   build.Build,
-			"date":    build.Date,
-		})
-	})
-
-	logger.Info("Loading observer API")
-
-	observerAPI := engine.Group("/observer/v1")
 
-	api.SetupObserverAPI(observerAPI, cache)
+	api.SetupObserverAPI(engine, cache)
 	internal.SetupGracefulShutdown(port, engine)
 }
diff --git a/cmd/platform_api/main.go b/cmd/platform_api/main.go
index fa58fc8..5072204 100644
--- a/cmd/platform_api/main.go
+++ b/cmd/platform_api/main.go
@@ -4,7 +4,6 @@ import (
 	"github.com/gin-gonic/gin"
 	"github.com/spf13/viper"
 	"github.com/trustwallet/blockatlas/api"
-	"github.com/trustwallet/blockatlas/build"
 	_ "github.com/trustwallet/blockatlas/docs"
 	"github.com/trustwallet/blockatlas/internal"
 	"github.com/trustwallet/blockatlas/pkg/ginutils"
@@ -17,35 +16,24 @@ const (
 )
 
 var (
-	port, confPath, chosenPlatform string
-	sg                             *gin.HandlerFunc
+	port, confPath string
+	sg             *gin.HandlerFunc
 )
 
 func init() {
-	build.LogVersionInfo()
 	port, confPath, sg = internal.InitAPI(defaultPort, defaultConfigPath)
 	platform.Init(viper.GetString("platform"))
 }
 
 func main() {
 	gin.SetMode(viper.GetString("gin.mode"))
+
 	engine := gin.New()
 	engine.Use(ginutils.CheckReverseProxy, *sg)
 	engine.Use(ginutils.CORSMiddleware())
 	engine.Use(gin.Logger())
-
 	engine.OPTIONS("/*path", ginutils.CORSMiddleware())
-	engine.GET("/", api.GetRoot)
-	engine.GET("/status", func(c *gin.Context) {
-		ginutils.RenderSuccess(c, map[string]interface{}{
-			"status":  true,
-			"version": build.Version,
-			"build":   build.Build,
-			"date":    build.Date,
-		})
-	})
-
-	api.LoadPlatforms(engine)
 
+	api.SetupPlatformAPI(engine)
 	internal.SetupGracefulShutdown(port, engine)
 }
diff --git a/cmd/platform_observer/main.go b/cmd/platform_observer/main.go
index a1e0a30..a04fdb2 100644
--- a/cmd/platform_observer/main.go
+++ b/cmd/platform_observer/main.go
@@ -3,7 +3,6 @@ package main
 import (
 	"context"
 	"github.com/spf13/viper"
-	"github.com/trustwallet/blockatlas/build"
 	"github.com/trustwallet/blockatlas/internal"
 	"github.com/trustwallet/blockatlas/observer"
 	"github.com/trustwallet/blockatlas/pkg/logger"
@@ -22,7 +21,6 @@ var (
 )
 
 func init() {
-	build.LogVersionInfo()
 	_, confPath, _, cache = internal.InitAPIWithRedis("", defaultConfigPath)
 	platform.Init(viper.GetString("platform"))
 }
diff --git a/cmd/swagger_api/main.go b/cmd/swagger_api/main.go
index 02c4f43..660821c 100644
--- a/cmd/swagger_api/main.go
+++ b/cmd/swagger_api/main.go
@@ -6,7 +6,6 @@ import (
 	ginSwagger "github.com/swaggo/gin-swagger"
 	"github.com/swaggo/gin-swagger/swaggerFiles"
 	"github.com/trustwallet/blockatlas/api"
-	"github.com/trustwallet/blockatlas/build"
 	_ "github.com/trustwallet/blockatlas/docs"
 	"github.com/trustwallet/blockatlas/internal"
 	"github.com/trustwallet/blockatlas/pkg/ginutils"
@@ -15,7 +14,6 @@ import (
 const (
 	defaultPort       = "8423"
 	defaultConfigPath = "../../config.yml"
-	allPlatforms      = "all"
 )
 
 var (
@@ -24,28 +22,19 @@ var (
 )
 
 func init() {
-	build.LogVersionInfo()
 	port, confPath, sg = internal.InitAPI(defaultPort, defaultConfigPath)
 }
 
 func main() {
 	gin.SetMode(viper.GetString("gin.mode"))
-
 	engine := gin.New()
+
 	engine.Use(ginutils.CheckReverseProxy, *sg)
 	engine.Use(ginutils.CORSMiddleware())
 
 	engine.OPTIONS("/*path", ginutils.CORSMiddleware())
 	engine.GET("/", api.GetRoot)
-	engine.GET("/status", func(c *gin.Context) {
-		ginutils.RenderSuccess(c, map[string]interface{}{
-			"status":  true,
-			"version": build.Version,
-			"build":   build.Build,
-			"date":    build.Date,
-		})
-	})
-
+	engine.GET("/status", api.GetStatus)
 	engine.GET("swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
 
 	internal.SetupGracefulShutdown(port, engine)
diff --git a/config.yml b/config.yml
index 7d232e4..8f71172 100644
--- a/config.yml
+++ b/config.yml
@@ -6,9 +6,10 @@ gin:
   # If set, HTTP Forwarded headers will be respected
   reverse_proxy: false
 
+# If all - run all platforms in one binary. You can pick specific coin handle to run binary only with specific coin
+# Example: ethereum
+# You can see all the coin handles at coins/coins.yml file
 platform: all
-#metrics:
-#  api_token: xxxxxx
 
 # Sentry error tracking
 #sentry:
@@ -16,8 +17,6 @@ platform: all
 
 # The transaction watcher
 observer:
-  enabled: false
-  # TODO : Add description
   auth: test
   # Don't request blocks older than this
   backlog: 3h
diff --git a/go.sum b/go.sum
index f5dc222..d9943f8 100644
--- a/go.sum
+++ b/go.sum
@@ -22,6 +22,7 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc
 github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
+github.com/Pantani/httpexpect v2.0.0+incompatible h1:P658jo0d3gej0D67KBcSUgpZY76CH1TdxvOfQAmyNu0=
 github.com/Pantani/httpexpect v2.0.0+incompatible/go.mod h1:+8VdK+EZPV0YorWMMNyakEIYKcDl9OHtde3DdkPqbwQ=
 github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
 github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
@@ -33,6 +34,7 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrU
 github.com/VictoriaMetrics/fastcache v1.5.3 h1:2odJnXLbFZcoV9KYtQ+7TH1UOq3dn3AssMgieaezkR4=
 github.com/VictoriaMetrics/fastcache v1.5.3/go.mod h1:+jv9Ckb+za/P1ZRg/sulP5Ni1v49daAVERr0H3CuscE=
 github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
+github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
 github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
@@ -122,6 +124,7 @@ github.com/ethereum/go-ethereum v1.9.10 h1:jooX7tWcscpC7ytufk73t9JMCeJQ7aJF2YmZJ
 github.com/ethereum/go-ethereum v1.9.10/go.mod h1:lXHkVo/MTvsEXfYsmNzelZ8R1e0DTvdk/wMZJIRpaRw=
 github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
 github.com/fatih/color v1.3.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/fjl/memsize v0.0.0-20180418122429-ca190fb6ffbc/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
 github.com/fjl/memsize v0.0.0-20180929194037-2a09253e352a/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0=
@@ -200,6 +203,7 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
@@ -229,6 +233,7 @@ github.com/huin/goupnp v0.0.0-20161224104101-679507af18f3/go.mod h1:MZ2ZmwcBpvOo
 github.com/huin/goupnp v1.0.0 h1:wg75sLpL6DZqwHQN6E1Cfk6mtfzS45z8OV+ic+DtHRo=
 github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc=
 github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
+github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
 github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
@@ -272,6 +277,7 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
 github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
 github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
+github.com/klauspost/compress v1.9.0 h1:GhthINjveNZAdFUD8QoQYfjxnOONZgztK/Yr6M23UTY=
 github.com/klauspost/compress v1.9.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -329,6 +335,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
 github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
 github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
 github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
@@ -404,6 +411,7 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114 h1:Pm6R878vxWWWR+Sa3ppsLce/Zq+JNTs6aVvRu13jv9A=
 github.com/shopspring/decimal v0.0.0-20200105231215-408a2507e114/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
@@ -474,7 +482,9 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY=
 github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
 github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
@@ -490,12 +500,17 @@ github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208 h1:1cngl9mPEoITZG8s8
 github.com/wsddn/go-ecdh v0.0.0-20161211032359-48726bab9208/go.mod h1:IotVbo4F+mw0EzQ08zFqg7pK3FebNXpaMsRy2RT+Ees=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
 github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
+github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
 github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
+github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
 github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
 github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
diff --git a/internal/init.go b/internal/init.go
index 319cc6e..6ac30c0 100644
--- a/internal/init.go
+++ b/internal/init.go
@@ -2,6 +2,7 @@ package internal
 
 import (
 	"flag"
+	"fmt"
 	sentrygin "github.com/getsentry/sentry-go/gin"
 	"github.com/gin-gonic/gin"
 	"github.com/spf13/viper"
@@ -9,6 +10,13 @@ import (
 	"github.com/trustwallet/blockatlas/pkg/logger"
 	"github.com/trustwallet/blockatlas/storage"
 	"path/filepath"
+	"runtime"
+	"time"
+)
+
+var (
+	Build = "dev"
+	Date  = time.Now().String()
 )
 
 func InitAPI(defaultPort, defaultConfigPath string) (string, string, *gin.HandlerFunc) {
@@ -17,6 +25,7 @@ func InitAPI(defaultPort, defaultConfigPath string) (string, string, *gin.Handle
 		sg             gin.HandlerFunc
 	)
 
+	LogVersionInfo()
 	sg = sentrygin.New(sentrygin.Options{})
 
 	flag.StringVar(&port, "p", defaultPort, "port for api")
@@ -40,7 +49,7 @@ func InitAPIWithRedis(defaultPort, defaultConfigPath string) (string, string, *g
 		cache          *storage.Storage
 		sg             gin.HandlerFunc
 	)
-
+	LogVersionInfo()
 	cache = storage.New()
 	sg = sentrygin.New(sentrygin.Options{})
 
@@ -63,3 +72,16 @@ func InitAPIWithRedis(defaultPort, defaultConfigPath string) (string, string, *g
 
 	return port, confPath, &sg, cache
 }
+
+func LogVersionInfo() {
+	fmt.Printf(`
+-------------------------------------------------------------------------------
+Build: %v
+Start date: %v
+OS: %s
+Go Arch: %s
+Go Version: %s
+-------------------------------------------------------------------------------
+`,
+		Build, Date, runtime.GOOS, runtime.GOARCH, runtime.Version())
+}
diff --git a/observer/dispatcher.go b/observer/dispatcher.go
index 7cb7b5c..45b07d0 100644
--- a/observer/dispatcher.go
+++ b/observer/dispatcher.go
@@ -47,8 +47,8 @@ func (d *Dispatcher) dispatch(event Event) {
 func (d *Dispatcher) postWebhook(hook string, data []byte, logParams logger.Params) {
 	_, err := d.Client.Post(hook, "application/json", bytes.NewReader(data))
 	if err != nil {
-		err = errors.E(err, errors.Params{"hook": hook}).PushToSentry()
-		logger.Error(err, "Failed to dispatch event", logger.Params{"webhook": hook}, logParams)
+		err = errors.E(err, "Failed to dispatch event", errors.Params{"webhook": hook}, logParams)
+		logger.Error(err, logger.Params{"webhook": hook}, logParams)
 	}
 	logger.Info("Webhook dispatched", logger.Params{"webhook": hook}, logParams)
 }
diff --git a/pkg/address/address.go b/pkg/address/address.go
index a720f8c..3f4ccc7 100644
--- a/pkg/address/address.go
+++ b/pkg/address/address.go
@@ -48,7 +48,7 @@ func HexToAddress(hexAddr string) (b58 string, err error) {
 	bytes, err := hex.DecodeString(hexAddr)
 	if err != nil {
 		return "", errors.E(err, errors.TypePlatformUnmarshal,
-			errors.Params{"hexAddr": hexAddr}).PushToSentry()
+			errors.Params{"hexAddr": hexAddr})
 	}
 	var checksum [32]byte
 	checksum = sha256.Sum256(bytes)
diff --git a/pkg/blockatlas/client.go b/pkg/blockatlas/client.go
index 19dfaab..c10c2dc 100644
--- a/pkg/blockatlas/client.go
+++ b/pkg/blockatlas/client.go
@@ -5,7 +5,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"github.com/trustwallet/blockatlas/pkg/errors"
-	"github.com/trustwallet/blockatlas/pkg/metrics"
 	"io"
 	"io/ioutil"
 	"net/http"
@@ -62,7 +61,6 @@ func (r *Request) Post(result interface{}, path string, body interface{}) error
 
 func (r *Request) Execute(method string, url string, body io.Reader, result interface{}, params errors.Params) error {
 	params["method"] = method
-	start := time.Now()
 	req, err := http.NewRequest(method, url, body)
 	if err != nil {
 		return errors.E(err, errors.TypePlatformRequest, params)
@@ -76,7 +74,6 @@ func (r *Request) Execute(method string, url string, body io.Reader, result inte
 	if err != nil {
 		return errors.E(err, errors.TypePlatformRequest, params)
 	}
-	go metrics.GetMetrics(res.Status, url, method, start)
 
 	err = r.ErrorHandler(res, url)
 	if err != nil {
diff --git a/pkg/blockatlas/clientcache.go b/pkg/blockatlas/clientcache.go
index 8473989..12ace94 100644
--- a/pkg/blockatlas/clientcache.go
+++ b/pkg/blockatlas/clientcache.go
@@ -68,7 +68,7 @@ func (mc *memCache) setCache(key string, value interface{}, duration time.Durati
 	defer mc.RUnlock()
 	b, err := json.Marshal(value)
 	if err != nil {
-		logger.Error(errors.E(err, "client cache cannot marshal cache object").PushToSentry())
+		logger.Error(errors.E(err, "client cache cannot marshal cache object"))
 		return
 	}
 	memoryCache.cache.Set(key, b, duration)
@@ -85,7 +85,7 @@ func (mc *memCache) getCache(key string, value interface{}) error {
 	}
 	err := json.Unmarshal(r, value)
 	if err != nil {
-		return errors.E(err, util.ErrNotFound).PushToSentry()
+		return errors.E(err, util.ErrNotFound)
 	}
 	return nil
 }
diff --git a/pkg/blockatlas/jsonrpc.go b/pkg/blockatlas/jsonrpc.go
index e250fbd..b9a187d 100644
--- a/pkg/blockatlas/jsonrpc.go
+++ b/pkg/blockatlas/jsonrpc.go
@@ -33,12 +33,12 @@ type RpcError struct {
 func (r *RpcResponse) GetObject(toType interface{}) error {
 	js, err := json.Marshal(r.Result)
 	if err != nil {
-		return errors.E(err, "json-rpc GetObject Marshal error", errors.Params{"obj": toType}).PushToSentry()
+		return errors.E(err, "json-rpc GetObject Marshal error", errors.Params{"obj": toType})
 	}
 
 	err = json.Unmarshal(js, toType)
 	if err != nil {
-		return errors.E(err, "json-rpc GetObject Unmarshal error", errors.Params{"obj": toType, "string": string(js)}).PushToSentry()
+		return errors.E(err, "json-rpc GetObject Unmarshal error", errors.Params{"obj": toType, "string": string(js)})
 	}
 	return nil
 }
diff --git a/pkg/blockatlas/marshal.go b/pkg/blockatlas/marshal.go
index f2b1183..00f6dd8 100644
--- a/pkg/blockatlas/marshal.go
+++ b/pkg/blockatlas/marshal.go
@@ -45,7 +45,7 @@ func (t *Tx) UnmarshalJSON(data []byte) error {
 	case TxAnyAction:
 		t.Meta = new(AnyAction)
 	default:
-		return errors.E("unsupported tx type", errors.Params{"type": t.Type}).PushToSentry()
+		return errors.E("unsupported tx type", errors.Params{"type": t.Type})
 	}
 	if err := json.Unmarshal(raw, t.Meta); err != nil {
 		return err
@@ -75,7 +75,7 @@ func (t *Tx) MarshalJSON() ([]byte, error) {
 	case AnyAction, *AnyAction:
 		t.Type = TxAnyAction
 	default:
-		return nil, errors.E("unsupported tx metadata", errors.Params{"meta": t.Meta}).PushToSentry()
+		return nil, errors.E("unsupported tx metadata", errors.Params{"meta": t.Meta})
 	}
 
 	// Set status to completed by default
@@ -97,7 +97,7 @@ func (a *Amount) UnmarshalJSON(data []byte) error {
 	}
 	str := string(n)
 	if !matchNumber.MatchString(str) {
-		return errors.E("not a regular decimal number", errors.Params{"str": str}).PushToSentry()
+		return errors.E("not a regular decimal number", errors.Params{"str": str})
 	}
 	if strings.ContainsRune(str, '.') {
 		str, _ = numbers.DecimalToSatoshis(str)
diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go
index 5a9019c..d2746cb 100644
--- a/pkg/errors/errors.go
+++ b/pkg/errors/errors.go
@@ -8,20 +8,20 @@ import (
 	"strings"
 )
 
-type Params map[string]interface{}
+type (
+	Params map[string]interface{}
 
-// Error represents a error's specification.
-type Error struct {
-	Err   error
-	Type  Type
-	meta  map[string]interface{}
-	stack []string
-}
-
-var (
-	_ error = (*Error)(nil)
+	// Error represents a error's specification.
+	Error struct {
+		Err   error
+		Type  Type
+		meta  map[string]interface{}
+		stack []string
+	}
 )
 
+var _ error = (*Error)(nil)
+
 func (e *Error) isEmpty() bool {
 	return e.meta == nil && e.Type == TypeNone && e.Err == nil
 }
diff --git a/pkg/ginutils/gincache/cache.go b/pkg/ginutils/gincache/cache.go
index bbe2670..1de5fca 100644
--- a/pkg/ginutils/gincache/cache.go
+++ b/pkg/ginutils/gincache/cache.go
@@ -115,7 +115,7 @@ func (mc *memCache) deleteCache(key string) {
 func (mc *memCache) setCache(k string, x interface{}, d time.Duration) {
 	b, err := json.Marshal(x)
 	if err != nil {
-		logger.Error(errors.E(err, "client cache cannot marshal cache object").PushToSentry())
+		logger.Error(errors.E(err, "client cache cannot marshal cache object"))
 		return
 	}
 	mc.RLock()
@@ -135,7 +135,7 @@ func (mc *memCache) getCache(key string) (cacheResponse, error) {
 	}
 	err := json.Unmarshal(r, &result)
 	if err != nil {
-		return result, errors.E(err, util.ErrNotFound).PushToSentry()
+		return result, errors.E(err, util.ErrNotFound)
 	}
 	return result, nil
 }
diff --git a/pkg/metrics/client.go b/pkg/metrics/client.go
deleted file mode 100644
index e7c5a55..0000000
--- a/pkg/metrics/client.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package metrics
-
-import (
-	"github.com/prometheus/client_golang/prometheus"
-	"time"
-)
-
-const (
-	clientNamespace = "client"
-)
-
-var (
-	clientLabels   = []string{"status", "endpoint", "method"}
-	clientReqCount = prometheus.NewCounterVec(
-		prometheus.CounterOpts{
-			Namespace: clientNamespace,
-			Name:      "http_request_count_total",
-			Help:      "Total number of HTTP requests made.",
-		}, clientLabels,
-	)
-	clientReqDuration = prometheus.NewHistogramVec(
-		prometheus.HistogramOpts{
-			Namespace: clientNamespace,
-			Name:      "http_request_duration_seconds",
-			Help:      "HTTP request latencies in seconds.",
-		}, clientLabels,
-	)
-)
-
-func GetMetrics(status, url, method string, start time.Time) {
-	endpoint := removeSensitiveInfo(url)
-	lvs := []string{status, endpoint, method}
-	clientReqCount.WithLabelValues(lvs...).Inc()
-	clientReqDuration.WithLabelValues(lvs...).Observe(time.Since(start).Seconds())
-}
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
deleted file mode 100644
index 4c7c2a5..0000000
--- a/pkg/metrics/metrics.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package metrics
-
-import (
-	"github.com/prometheus/client_golang/prometheus"
-	"regexp"
-)
-
-// init registers the prometheus metrics
-func init() {
-	prometheus.MustRegister(clientReqCount, clientReqDuration, serverReqCount, serverReqDuration, serverReqSizeBytes, serverRespSizeBytes)
-}
-
-func removeSensitiveInfo(info string) string {
-	reg := regexp.MustCompile(`([a-zA-Z0-9\s]{30,})|([0-9]{4,})|(=(.*?)[^(&|$)]+)|(--[^$]+)|(&asset_contract_addresses)`)
-	return reg.ReplaceAllString(info, "")
-}
diff --git a/pkg/metrics/server.go b/pkg/metrics/server.go
deleted file mode 100644
index f95777e..0000000
--- a/pkg/metrics/server.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package metrics
-
-import (
-	"fmt"
-	"github.com/gin-gonic/gin"
-	"github.com/prometheus/client_golang/prometheus"
-	"net/http"
-	"time"
-)
-
-const (
-	serverNamespace = "server"
-)
-
-var (
-	serverLabels   = []string{"status", "endpoint", "method"}
-	serverReqCount = prometheus.NewCounterVec(
-		prometheus.CounterOpts{
-			Namespace: serverNamespace,
-			Name:      "http_request_count_total",
-			Help:      "Total number of HTTP requests made.",
-		}, serverLabels,
-	)
-	serverReqDuration = prometheus.NewHistogramVec(
-		prometheus.HistogramOpts{
-			Namespace: serverNamespace,
-			Name:      "http_request_duration_seconds",
-			Help:      "HTTP request latencies in seconds.",
-		}, serverLabels,
-	)
-
-	serverReqSizeBytes = prometheus.NewSummaryVec(
-		prometheus.SummaryOpts{
-			Namespace: serverNamespace,
-			Name:      "http_request_size_bytes",
-			Help:      "HTTP request sizes in bytes.",
-		}, serverLabels,
-	)
-	serverRespSizeBytes = prometheus.NewSummaryVec(
-		prometheus.SummaryOpts{
-			Namespace: serverNamespace,
-			Name:      "http_response_size_bytes",
-			Help:      "HTTP request sizes in bytes.",
-		}, serverLabels,
-	)
-)
-
-func PromMiddleware() gin.HandlerFunc {
-	return func(c *gin.Context) {
-		start := time.Now()
-		c.Next()
-
-		status := fmt.Sprintf("%d", c.Writer.Status())
-		url := c.Request.URL.Path
-		method := c.Request.Method
-
-		endpoint := removeSensitiveInfo(url)
-		lvs := []string{status, endpoint, method}
-
-		serverReqCount.WithLabelValues(lvs...).Inc()
-		serverReqDuration.WithLabelValues(lvs...).Observe(time.Since(start).Seconds())
-		serverReqSizeBytes.WithLabelValues(lvs...).Observe(calcRequestSize(c.Request))
-		serverRespSizeBytes.WithLabelValues(lvs...).Observe(float64(c.Writer.Size()))
-	}
-}
-
-func calcRequestSize(r *http.Request) float64 {
-	size := 0
-	if r.URL != nil {
-		size = len(r.URL.String())
-	}
-
-	size += len(r.Method)
-	size += len(r.Proto)
-
-	for name, values := range r.Header {
-		size += len(name)
-		for _, value := range values {
-			size += len(value)
-		}
-	}
-	size += len(r.Host)
-
-	// r.Form and r.MultipartForm are assumed to be included in r.URL.
-	if r.ContentLength != -1 {
-		size += int(r.ContentLength)
-	}
-	return float64(size)
-}
diff --git a/pkg/numbers/decimal.go b/pkg/numbers/decimal.go
index 51ce48a..667470a 100644
--- a/pkg/numbers/decimal.go
+++ b/pkg/numbers/decimal.go
@@ -15,7 +15,7 @@ func DecimalToSatoshis(dec string) (string, error) {
 	out = strings.TrimLeft(out, "0")
 	for _, c := range out {
 		if !unicode.IsNumber(c) {
-			return "", errors.E("not a number", errors.Params{"dec": dec, "c": c}).PushToSentry()
+			return "", errors.E("not a number", errors.Params{"dec": dec, "c": c})
 		}
 	}
 	return out, nil
@@ -64,7 +64,7 @@ func DecimalExp(dec string, exp int) string {
 func HexToDecimal(hex string) (string, error) {
 	var i big.Int
 	if _, ok := i.SetString(hex, 0); !ok {
-		return "", errors.E("invalid hex", errors.Params{"hex": hex}).PushToSentry()
+		return "", errors.E("invalid hex", errors.Params{"hex": hex})
 	}
 	return i.String(), nil
 }
diff --git a/platform/binance/block.go b/platform/binance/block.go
index 9115c6c..1a93421 100644
--- a/platform/binance/block.go
+++ b/platform/binance/block.go
@@ -14,7 +14,7 @@ func (p *Platform) CurrentBlockNumber() (int64, error) {
 		return 0, err
 	}
 	if len(list.BlockArray) == 0 {
-		return 0, errors.E("no block descriptor found", errors.TypePlatformApi).PushToSentry()
+		return 0, errors.E("no block descriptor found", errors.TypePlatformApi)
 	}
 	return list.BlockArray[0].BlockHeight, nil
 }
diff --git a/platform/binance/client.go b/platform/binance/client.go
index 5a2cb25..cd1b2f1 100644
--- a/platform/binance/client.go
+++ b/platform/binance/client.go
@@ -57,7 +57,7 @@ func getHTTPError(res *http.Response, desc string) error {
 	case http.StatusOK:
 		return nil
 	default:
-		return errors.E("getHTTPError error", errors.Params{"status": res.Status}).PushToSentry()
+		return errors.E("getHTTPError error", errors.Params{"status": res.Status})
 	}
 }
 
@@ -65,7 +65,7 @@ func getAPIError(res *http.Response, desc string) error {
 	var sErr Error
 	err := json.NewDecoder(res.Body).Decode(&sErr)
 	if err != nil {
-		err = errors.E(err, errors.TypePlatformUnmarshal, errors.Params{"desc": desc}).PushToSentry()
+		err = errors.E(err, errors.TypePlatformUnmarshal, errors.Params{"desc": desc})
 		logger.Error(err, "Binance: Failed to decode error response")
 		return blockatlas.ErrSourceConn
 	}
diff --git a/platform/cosmos/client.go b/platform/cosmos/client.go
index ea59ad1..eee6f4b 100644
--- a/platform/cosmos/client.go
+++ b/platform/cosmos/client.go
@@ -51,7 +51,7 @@ func (c *Client) CurrentBlockNumber() (num int64, err error) {
 
 	num, err = strconv.ParseInt(block.Meta.Header.Height, 10, 64)
 	if err != nil {
-		return num, errors.E("error to ParseInt", errors.TypePlatformUnmarshal).PushToSentry()
+		return num, errors.E("error to ParseInt", errors.TypePlatformUnmarshal)
 	}
 
 	return
diff --git a/platform/cosmos/stake.go b/platform/cosmos/stake.go
index fe919f0..94795bd 100644
--- a/platform/cosmos/stake.go
+++ b/platform/cosmos/stake.go
@@ -32,7 +32,7 @@ func (p *Platform) GetValidators() (blockatlas.ValidatorPage, error) {
 	}
 	inflationValue, err := strconv.ParseFloat(inflation.Result, 32)
 	if err != nil {
-		return nil, errors.E("error to parse inflationValue to float", errors.TypePlatformUnmarshal).PushToSentry()
+		return nil, errors.E("error to parse inflationValue to float", errors.TypePlatformUnmarshal)
 	}
 
 	for _, validator := range validators.Result {
diff --git a/platform/ethereum/base.go b/platform/ethereum/base.go
index d830406..4d78bcd 100644
--- a/platform/ethereum/base.go
+++ b/platform/ethereum/base.go
@@ -12,15 +12,19 @@ type Platform struct {
 	collectionsClient CollectionsClient
 }
 
-func Init(coin uint, api, rpc, collectionApi, collectionKey string) *Platform {
-	p := Platform{
-		CoinIndex:         coin,
-		RpcURL:            rpc,
-		client:            Client{blockatlas.InitClient(api)},
-		collectionsClient: CollectionsClient{blockatlas.InitClient(collectionApi)},
+func Init(coin uint, api, rpc string) *Platform {
+	return &Platform{
+		CoinIndex: coin,
+		RpcURL:    rpc,
+		client:    Client{blockatlas.InitClient(api)},
 	}
+}
+
+func InitWitCollection(coin uint, api, rpc, collectionApi, collectionKey string) *Platform {
+	p := Init(coin, api, rpc)
+	p.collectionsClient = CollectionsClient{blockatlas.InitClient(collectionApi)}
 	p.collectionsClient.Headers["X-API-KEY"] = collectionKey
-	return &p
+	return p
 }
 
 func (p *Platform) Coin() coin.Coin {
diff --git a/platform/ethereum/collection_client.go b/platform/ethereum/collection_client.go
index 39cadf6..c41ae63 100644
--- a/platform/ethereum/collection_client.go
+++ b/platform/ethereum/collection_client.go
@@ -28,7 +28,7 @@ func (c CollectionsClient) GetCollectibles(owner string, collectibleID string) (
 	collection := searchCollection(collections, collectibleID)
 	if collection == nil {
 		return nil, nil, errors.E("collectible not found", errors.TypePlatformClient,
-			errors.Params{"collectibleID": collectibleID}).PushToSentry()
+			errors.Params{"collectibleID": collectibleID})
 	}
 
 	query := url.Values{
@@ -53,7 +53,7 @@ func (c CollectionsClient) OldGetCollectibles(owner string, collectibleID string
 	collection := oldSearchCollection(collections, id)
 	if collection == nil {
 		return nil, nil, errors.E("collectible not found", errors.TypePlatformClient,
-			errors.Params{"collectibleID": collectibleID}).PushToSentry()
+			errors.Params{"collectibleID": collectibleID})
 	}
 
 	query := url.Values{
diff --git a/platform/harmony/transaction.go b/platform/harmony/transaction.go
index a83a6dd..890b5b4 100644
--- a/platform/harmony/transaction.go
+++ b/platform/harmony/transaction.go
@@ -29,7 +29,7 @@ func NormalizeTxs(txs []Transaction) blockatlas.TxPage {
 }
 
 func GetNormalizationError(err error) error {
-	return errors.E(err, errors.TypePlatformNormalize, errors.Params{"method": "Harmony_NormalizeTx"}).PushToSentry()
+	return errors.E(err, errors.TypePlatformNormalize, errors.Params{"method": "Harmony_NormalizeTx"})
 }
 
 func NormalizeTx(trx *Transaction) (tx blockatlas.Tx, b bool, err error) {
diff --git a/platform/icon/transaction.go b/platform/icon/transaction.go
index 0742e94..3506092 100644
--- a/platform/icon/transaction.go
+++ b/platform/icon/transaction.go
@@ -31,7 +31,7 @@ func (p *Platform) GetTxsByAddress(address string) (blockatlas.TxPage, error) {
 func Normalize(trx *Tx) (tx blockatlas.Tx, b bool) {
 	date, err := time.Parse("2006-01-02T15:04:05.999Z0700", trx.CreateDate)
 	if err != nil {
-		err = errors.E(err, errors.TypePlatformUnmarshal).PushToSentry()
+		err = errors.E(err, errors.TypePlatformUnmarshal)
 		logger.Error(err)
 		return tx, false
 	}
diff --git a/platform/iotex/client.go b/platform/iotex/client.go
index c06b337..5e86adb 100644
--- a/platform/iotex/client.go
+++ b/platform/iotex/client.go
@@ -21,7 +21,7 @@ func (c *Client) GetLatestBlock() (int64, error) {
 	}
 	b, err := strconv.ParseInt(chainMeta.Height, 10, 64)
 	if err != nil {
-		return 0, errors.E(err, "ParseInt failed", errors.TypePlatformUnmarshal).PushToSentry()
+		return 0, errors.E(err, "ParseInt failed", errors.TypePlatformUnmarshal)
 	}
 	return b, nil
 }
diff --git a/platform/nano/transaction.go b/platform/nano/transaction.go
index 2e9ff1b..c509b8b 100644
--- a/platform/nano/transaction.go
+++ b/platform/nano/transaction.go
@@ -24,14 +24,17 @@ func (p *Platform) GetTxsByAddress(address string) (blockatlas.TxPage, error) {
 	}
 
 	for _, srcTx := range txs {
-		tx := p.Normalize(&srcTx, history.Account)
+		tx, err := p.Normalize(&srcTx, history.Account)
+		if err != nil {
+			continue
+		}
 		normalized = append(normalized, tx)
 	}
 
 	return normalized, nil
 }
 
-func (p *Platform) Normalize(srcTx *Transaction, account string) (tx blockatlas.Tx) {
+func (p *Platform) Normalize(srcTx *Transaction, account string) (blockatlas.Tx, error) {
 	var from string
 	var to string
 
@@ -44,13 +47,19 @@ func (p *Platform) Normalize(srcTx *Transaction, account string) (tx blockatlas.
 	}
 
 	status := blockatlas.StatusCompleted
-	height, _ := strconv.ParseUint(srcTx.Height, 10, 64)
+	height, err := strconv.ParseUint(srcTx.Height, 10, 64)
+	if err != nil {
+		return blockatlas.Tx{}, err
+	}
 	if height == 0 {
 		status = blockatlas.StatusPending
 	}
-	timestamp, _ := strconv.ParseInt(srcTx.LocalTimestamp, 10, 64)
+	timestamp, err := strconv.ParseInt(srcTx.LocalTimestamp, 10, 64)
+	if err != nil {
+		return blockatlas.Tx{}, err
+	}
 
-	tx = blockatlas.Tx{
+	tx := blockatlas.Tx{
 		ID:     srcTx.Hash,
 		Coin:   p.Coin().ID,
 		Date:   timestamp,
@@ -65,5 +74,5 @@ func (p *Platform) Normalize(srcTx *Transaction, account string) (tx blockatlas.
 			Decimals: p.Coin().Decimals,
 		},
 	}
-	return tx
+	return tx, nil
 }
diff --git a/platform/platform.go b/platform/platform.go
index 2ef29fb..60267e0 100644
--- a/platform/platform.go
+++ b/platform/platform.go
@@ -31,7 +31,11 @@ import (
 	"github.com/trustwallet/blockatlas/platform/zilliqa"
 )
 
-const emptyParam = ""
+const (
+	allPlatformsHandle = "all"
+)
+
+var CollectionsWhitelist map[uint]bool
 
 func GetVar(name string) string {
 	return viper.GetString(name)
@@ -87,13 +91,18 @@ func getPlatformMap() blockatlas.Platforms {
 		coin.Dash().Handle:         bitcoin.Init(coin.DASH, GetApiVar(coin.DASH)),
 		coin.Doge().Handle:         bitcoin.Init(coin.DOGE, GetApiVar(coin.DOGE)),
 		coin.Qtum().Handle:         bitcoin.Init(coin.QTUM, GetApiVar(coin.QTUM)),
-		coin.Gochain().Handle:      ethereum.Init(coin.GO, GetApiVar(coin.GO), GetRpcVar(coin.GO), emptyParam, emptyParam),
-		coin.Thundertoken().Handle: ethereum.Init(coin.TT, GetApiVar(coin.TT), GetRpcVar(coin.TT), emptyParam, emptyParam),
-		coin.Classic().Handle:      ethereum.Init(coin.ETC, GetApiVar(coin.ETC), GetRpcVar(coin.ETC), emptyParam, emptyParam),
-		coin.Poa().Handle:          ethereum.Init(coin.POA, GetApiVar(coin.POA), GetRpcVar(coin.POA), emptyParam, emptyParam),
-		coin.Callisto().Handle:     ethereum.Init(coin.CLO, GetApiVar(coin.CLO), GetRpcVar(coin.CLO), emptyParam, emptyParam),
-		coin.Wanchain().Handle:     ethereum.Init(coin.WAN, GetApiVar(coin.WAN), GetRpcVar(coin.WAN), emptyParam, emptyParam),
-		coin.Tomochain().Handle:    ethereum.Init(coin.TOMO, GetApiVar(coin.TOMO), GetRpcVar(coin.TOMO), emptyParam, emptyParam),
-		coin.Ethereum().Handle:     ethereum.Init(coin.ETH, GetApiVar(coin.ETH), GetRpcVar(coin.ETH), GetVar("ethereum.collections_api"), GetVar("ethereum.collections_api_key")),
+		coin.Gochain().Handle:      ethereum.Init(coin.GO, GetApiVar(coin.GO), GetRpcVar(coin.GO)),
+		coin.Thundertoken().Handle: ethereum.Init(coin.TT, GetApiVar(coin.TT), GetRpcVar(coin.TT)),
+		coin.Classic().Handle:      ethereum.Init(coin.ETC, GetApiVar(coin.ETC), GetRpcVar(coin.ETC)),
+		coin.Poa().Handle:          ethereum.Init(coin.POA, GetApiVar(coin.POA), GetRpcVar(coin.POA)),
+		coin.Callisto().Handle:     ethereum.Init(coin.CLO, GetApiVar(coin.CLO), GetRpcVar(coin.CLO)),
+		coin.Wanchain().Handle:     ethereum.Init(coin.WAN, GetApiVar(coin.WAN), GetRpcVar(coin.WAN)),
+		coin.Tomochain().Handle:    ethereum.Init(coin.TOMO, GetApiVar(coin.TOMO), GetRpcVar(coin.TOMO)),
+		coin.Ethereum().Handle:     ethereum.InitWitCollection(coin.ETH, GetApiVar(coin.ETH), GetRpcVar(coin.ETH), GetVar("ethereum.collections_api"), GetVar("ethereum.collections_api_key")),
 	}
 }
+
+func InitCollectionsWhitelist() {
+	CollectionsWhitelist = make(map[uint]bool)
+	CollectionsWhitelist[coin.Ethereum().ID] = true
+}
diff --git a/platform/registry.go b/platform/registry.go
index b4fb817..a101835 100644
--- a/platform/registry.go
+++ b/platform/registry.go
@@ -28,23 +28,28 @@ var (
 )
 
 func getActivePlatforms(handle string) []blockatlas.Platform {
-	logger.Info("Loaded with: ", logger.Params{"handle": handle})
-
 	platforms := getPlatformMap()
+	logger.Info("Platform API setup with: ", logger.Params{"handle": handle})
+
+	if handle == allPlatformsHandle {
+		return platforms.GetPlatformList()
+	}
+
 	platform, ok := platforms[handle]
 	if ok {
 		return []blockatlas.Platform{platform}
-	} else if handle == "all" {
-		return platforms.GetPlatformList()
-	} else {
-		logger.Fatal("Please, use ATLAS_PLATFORM handle with non-empty value", logger.Params{"ATLAS_PLATFORM": handle})
-		return nil
 	}
+
+	logger.Fatal("Please, use ATLAS_PLATFORM handle with non-empty value, see more at Readme. Example: all", logger.Params{"ATLAS_PLATFORM": handle})
+	return nil
 }
 
 func Init(platformHandle string) {
 	platformList := getActivePlatforms(platformHandle)
 
+	// white list of collection api coins (only ETH now)
+	InitCollectionsWhitelist()
+
 	Platforms = make(map[string]blockatlas.Platform)
 	BlockAPIs = make(map[string]blockatlas.BlockAPI)
 	StakeAPIs = make(map[string]blockatlas.StakeAPI)
@@ -84,7 +89,7 @@ func Init(platformHandle string) {
 		if namingAPI, ok := platform.(blockatlas.NamingServiceAPI); ok {
 			NamingAPIs[uint64(platform.Coin().ID)] = namingAPI
 		}
-		if collectionAPI, ok := platform.(blockatlas.CollectionAPI); ok {
+		if collectionAPI, ok := platform.(blockatlas.CollectionAPI); ok && CollectionsWhitelist[platform.Coin().ID] {
 			CollectionAPIs[platform.Coin().ID] = collectionAPI
 		}
 	}
diff --git a/platform/stellar/client.go b/platform/stellar/client.go
index eb47350..58716dd 100644
--- a/platform/stellar/client.go
+++ b/platform/stellar/client.go
@@ -49,7 +49,7 @@ func (c *Client) CurrentBlockNumber() (int64, error) {
 	}
 
 	if len(ledgers.Embedded.Records) == 0 {
-		return 0, errors.E("CurrentBlockNumber: Records is empty", errors.TypePlatformUnmarshal).PushToSentry()
+		return 0, errors.E("CurrentBlockNumber: Records is empty", errors.TypePlatformUnmarshal)
 	}
 	return ledgers.Embedded.Records[0].Sequence, nil
 }
diff --git a/platform/tron/transaction.go b/platform/tron/transaction.go
index d12a25a..baf200c 100644
--- a/platform/tron/transaction.go
+++ b/platform/tron/transaction.go
@@ -35,7 +35,7 @@ func (p *Platform) GetTokenTxsByAddress(address, token string) (blockatlas.TxPag
 	tokenTxs, err := p.client.GetTxsOfAddress(address, token)
 	if err != nil {
 		return nil, errors.E(err, "TRON: failed to get token from address", errors.TypePlatformApi,
-			errors.Params{"address": address, "token": token}).PushToSentry()
+			errors.Params{"address": address, "token": token})
 	}
 
 	txs := make(blockatlas.TxPage, 0)
@@ -47,7 +47,7 @@ func (p *Platform) GetTokenTxsByAddress(address, token string) (blockatlas.TxPag
 	info, err := p.client.GetTokenInfo(token)
 	if err != nil || len(info.Data) == 0 {
 		return nil, errors.E(err, "TRON: failed to get token info", errors.TypePlatformApi,
-			errors.Params{"address": address, "token": token}).PushToSentry()
+			errors.Params{"address": address, "token": token})
 	}
 
 	for _, srcTx := range tokenTxs {
@@ -80,7 +80,7 @@ func setTokenMeta(tx *blockatlas.Tx, srcTx Tx, tokenInfo AssetInfo) {
 func Normalize(srcTx Tx) (*blockatlas.Tx, error) {
 	if len(srcTx.Data.Contracts) == 0 {
 		return nil, errors.E("TRON: transfer without contract", errors.TypePlatformApi,
-			errors.Params{"tx": srcTx}).PushToSentry()
+			errors.Params{"tx": srcTx})
 	}
 
 	contract := srcTx.Data.Contracts[0]
@@ -93,12 +93,12 @@ func Normalize(srcTx Tx) (*blockatlas.Tx, error) {
 	from, err := address.HexToAddress(transfer.OwnerAddress)
 	if err != nil {
 		return nil, errors.E(err, "TRON: failed to get from address", errors.TypePlatformApi,
-			errors.Params{"tx": srcTx}).PushToSentry()
+			errors.Params{"tx": srcTx})
 	}
 	to, err := address.HexToAddress(transfer.ToAddress)
 	if err != nil {
 		return nil, errors.E(err, "TRON: failed to get to address", errors.TypePlatformApi,
-			errors.Params{"tx": srcTx}).PushToSentry()
+			errors.Params{"tx": srcTx})
 	}
 
 	return &blockatlas.Tx{
diff --git a/platform/vechain/transaction.go b/platform/vechain/transaction.go
index bdf4a7e..119049b 100644
--- a/platform/vechain/transaction.go
+++ b/platform/vechain/transaction.go
@@ -54,19 +54,19 @@ func (p *Platform) getTransactionChannel(id string, txChan chan blockatlas.TxPag
 	srcTx, err := p.client.GetTransactionByID(id)
 	if err != nil {
 		return errors.E(err, "Failed to get tx", errors.TypePlatformUnmarshal,
-			errors.Params{"id": id}).PushToSentry()
+			errors.Params{"id": id})
 	}
 
 	receipt, err := p.client.GetTransactionReceiptByID(id)
 	if err != nil {
 		return errors.E(err, "Failed to get tx id receipt", errors.TypePlatformUnmarshal,
-			errors.Params{"id": id}).PushToSentry()
+			errors.Params{"id": id})
 	}
 
 	txs, err := p.NormalizeTokenTransaction(srcTx, receipt)
 	if err != nil {
 		return errors.E(err, "Failed to NormalizeBlockTransactions tx", errors.TypePlatformUnmarshal,
-			errors.Params{"tx": srcTx}).PushToSentry()
+			errors.Params{"tx": srcTx})
 	}
 	txChan <- txs
 	return nil
@@ -74,7 +74,7 @@ func (p *Platform) getTransactionChannel(id string, txChan chan blockatlas.TxPag
 
 func (p *Platform) NormalizeTokenTransaction(srcTx Tx, receipt TxReceipt) (blockatlas.TxPage, error) {
 	if receipt.Outputs == nil || len(receipt.Outputs) == 0 {
-		return blockatlas.TxPage{}, errors.E("NormalizeBlockTransaction: Clauses not found", errors.Params{"tx": srcTx}).PushToSentry()
+		return blockatlas.TxPage{}, errors.E("NormalizeBlockTransaction: Clauses not found", errors.Params{"tx": srcTx})
 	}
 
 	origin := address.EIP55Checksum(blockatlas.GetValidParameter(srcTx.Origin, srcTx.Meta.TxOrigin))
diff --git a/services/assets/info.go b/services/assets/info.go
deleted file mode 100644
index ca33c29..0000000
--- a/services/assets/info.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package assets
-
-import (
-	"github.com/trustwallet/blockatlas/coin"
-	"github.com/trustwallet/blockatlas/pkg/blockatlas"
-	"github.com/trustwallet/blockatlas/pkg/errors"
-	"time"
-)
-
-func GetCoinInfo(coinId int, token string) (info *blockatlas.CoinInfo, err error) {
-	c, ok := coin.Coins[uint(coinId)]
-	if !ok {
-		return info, errors.E("coin not found")
-	}
-	url := getCoinInfoUrl(c, token)
-	request := blockatlas.InitClient(url)
-	err = request.GetWithCache(&info, "/info.json", nil, time.Hour*1)
-	return
-}
-
-func getCoinInfoUrl(c coin.Coin, token string) string {
-	if len(token) == 0 {
-		return AssetsURL + c.Handle + "/info"
-	}
-	return AssetsURL + c.Handle + "/assets/" + token
-}
diff --git a/services/assets/model.go b/services/assets/model.go
index 1eed6ee..6d5ec90 100644
--- a/services/assets/model.go
+++ b/services/assets/model.go
@@ -1,24 +1,27 @@
 package assets
 
-type AssetValidators []AssetValidator
-type AssetValidatorMap map[string]AssetValidator
+type (
+	AssetValidators []AssetValidator
 
-type AssetValidator struct {
-	ID          string          `json:"id"`
-	Name        string          `json:"name"`
-	Description string          `json:"description"`
-	Website     string          `json:"website"`
-	Payout      ValidatorPayout `json:"payout,omitempty"`
-	Status      ValidatorStatus `json:"status,omitempty"`
-}
+	AssetValidatorMap map[string]AssetValidator
 
-type ValidatorPayout struct {
-	Commission float64 `json:"commission"`
-}
+	AssetValidator struct {
+		ID          string          `json:"id"`
+		Name        string          `json:"name"`
+		Description string          `json:"description"`
+		Website     string          `json:"website"`
+		Payout      ValidatorPayout `json:"payout,omitempty"`
+		Status      ValidatorStatus `json:"status,omitempty"`
+	}
 
-type ValidatorStatus struct {
-	Disabled bool `json:"disabled"`
-}
+	ValidatorPayout struct {
+		Commission float64 `json:"commission"`
+	}
+
+	ValidatorStatus struct {
+		Disabled bool `json:"disabled"`
+	}
+)
 
 func (av AssetValidators) toMap() AssetValidatorMap {
 	validators := make(AssetValidatorMap)


Test patch:
diff --git a/coin/gen_test.go b/coin/gen_test.go
index 067bee0..45e8f47 100644
--- a/coin/gen_test.go
+++ b/coin/gen_test.go
@@ -34,6 +34,9 @@ func TestFilesExists(t *testing.T) {
 func TestCoinFile(t *testing.T) {
 	var coinList []TestCoin
 	coin, err := os.Open(coinFile)
+	if err != nil {
+		t.Error(err)
+	}
 	dec := yaml.NewDecoder(coin)
 	err = dec.Decode(&coinList)
 	if err != nil {
diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go
deleted file mode 100644
index debf025..0000000
--- a/pkg/metrics/metrics_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package metrics
-
-import "testing"
-
-func Test_removeSensitiveInfo(t *testing.T) {
-	tests := []struct {
-		name string
-		info string
-		want string
-	}{
-		{"Remove Nimiq address", "/v1/nimiq/NQ43 J7G6 K6T8 H5KJ 5CXN Q5JK 2GJ4 6DSB 7PUH", "/v1/nimiq/"},
-		{"Remove Tezos address", "/v1/tezos/tz1WCd2jm4uSt4vntk4vSuUWoZQGhLcDuR9q", "/v1/tezos/"},
-		{"Remove Tron info", "https://api.trongrid.io/v1/accounts/TPJYCz8ppZNyvw7pTwmjajcx4Kk1MmEUhD/transactions?limit=200&only_confirmed=true&token_id=1000011", "https://api.trongrid.io/v1/accounts//transactions?limit&only_confirmed&token_id"},
-		{"Remove asset id", "https://api.trongrid.io/v1/assets/1000570?", "https://api.trongrid.io/v1/assets/?"},
-		{"Remove collection id", "/v2/ethereum/collections//collection/---enjin-old", "/v2/ethereum/collections//collection/"},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := removeSensitiveInfo(tt.info); got != tt.want {
-				t.Errorf("removeSensitiveInfo() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
diff --git a/platform/nano/transaction_test.go b/platform/nano/transaction_test.go
index db0e92b..84c4ce1 100644
--- a/platform/nano/transaction_test.go
+++ b/platform/nano/transaction_test.go
@@ -79,7 +79,7 @@ func TestNormalize(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if gotTx := platform.Normalize(tt.args.srcTx, tt.args.account); !reflect.DeepEqual(gotTx, tt.wantTx) {
+			if gotTx, err := platform.Normalize(tt.args.srcTx, tt.args.account); !reflect.DeepEqual(gotTx, tt.wantTx) && err == nil {
 				t.Errorf("Normalize() = %v, want %v", gotTx, tt.wantTx)
 			}
 		})
diff --git a/scripts/run_tests_postman.sh b/scripts/run_tests_postman.sh
new file mode 100755
index 0000000..0877438
--- /dev/null
+++ b/scripts/run_tests_postman.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# blockatlas host
+HOST=$1
+
+cd ..
+make newman test=transaction host=$HOST
+make newman test=token host=$1
+make newman test=staking host=$1
+make newman test=collection host=$1
+make newman test=domain host=$1
+make newman test=observer host=$1
diff --git a/services/assets/info_test.go b/services/assets/info_test.go
deleted file mode 100644
index bc7a280..0000000
--- a/services/assets/info_test.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package assets
-
-import (
-	"github.com/trustwallet/blockatlas/coin"
-	"testing"
-)
-
-func Test_getCoinInfoUrl(t *testing.T) {
-	type args struct {
-		c     coin.Coin
-		token string
-	}
-	tests := []struct {
-		name string
-		args args
-		want string
-	}{
-		{"test Ethereum coin", args{coin.Ethereum(), ""}, AssetsURL + coin.Ethereum().Handle + "/info"},
-		{"test Ethereum token", args{coin.Ethereum(), "0x0000000000b3F879cb30FE243b4Dfee438691c04"}, AssetsURL + coin.Ethereum().Handle + "/assets/" + "0x0000000000b3F879cb30FE243b4Dfee438691c04"},
-		{"test Binance coin", args{coin.Binance(), ""}, AssetsURL + coin.Binance().Handle + "/info"},
-		{"test Binance token", args{coin.Binance(), "BUSD-BD1"}, AssetsURL + coin.Binance().Handle + "/assets/" + "BUSD-BD1"},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			if got := getCoinInfoUrl(tt.args.c, tt.args.token); got != tt.want {
-				t.Errorf("getCoinInfoUrl() = %v, want %v", got, tt.want)
-			}
-		})
-	}
-}
diff --git a/storage/subscriptions.go b/storage/addresses.go
similarity index 100%
rename from storage/subscriptions.go
rename to storage/addresses.go
diff --git a/storage/block.go b/storage/tracker.go
similarity index 100%
rename from storage/block.go
rename to storage/tracker.go
diff --git a/pkg/tests/functional/fixtures.go b/tests/functional/fixtures.go
similarity index 100%
rename from pkg/tests/functional/fixtures.go
rename to tests/functional/fixtures.go
diff --git a/pkg/tests/functional/functional_test.go b/tests/functional/functional_test.go
similarity index 89%
rename from pkg/tests/functional/functional_test.go
rename to tests/functional/functional_test.go
index 96f7a86..d5b1df9 100755
--- a/pkg/tests/functional/functional_test.go
+++ b/tests/functional/functional_test.go
@@ -51,14 +51,7 @@ func TestApis(t *testing.T) {
 		})
 	})
 
-	api.MakeMetricsRoute(engine)
-	api.LoadPlatforms(engine)
-
-	if viper.GetBool("observer.enabled") {
-		logger.Info("Loading observer API")
-		observerAPI := engine.Group("/observer/v1")
-		api.SetupObserverAPI(observerAPI, cache)
-	}
+	api.SetupPlatformAPI(engine)
 
 	signalForExit := make(chan os.Signal, 1)
 
diff --git a/pkg/tests/functional/http.go b/tests/functional/http.go
similarity index 100%
rename from pkg/tests/functional/http.go
rename to tests/functional/http.go
diff --git a/pkg/tests/functional/testdata/body_fixtures.json b/tests/functional/testdata/body_fixtures.json
similarity index 100%
rename from pkg/tests/functional/testdata/body_fixtures.json
rename to tests/functional/testdata/body_fixtures.json
diff --git a/pkg/tests/functional/testdata/coin_fixtures.json b/tests/functional/testdata/coin_fixtures.json
similarity index 100%
rename from pkg/tests/functional/testdata/coin_fixtures.json
rename to tests/functional/testdata/coin_fixtures.json
diff --git a/pkg/tests/functional/testdata/exclude.json b/tests/functional/testdata/exclude.json
similarity index 100%
rename from pkg/tests/functional/testdata/exclude.json
rename to tests/functional/testdata/exclude.json
diff --git a/pkg/tests/functional/testdata/query_fixtures.json b/tests/functional/testdata/query_fixtures.json
similarity index 100%
rename from pkg/tests/functional/testdata/query_fixtures.json
rename to tests/functional/testdata/query_fixtures.json
diff --git a/pkg/tests/integration/bitcoin/block.go b/tests/integration/bitcoin/block.go
similarity index 100%
rename from pkg/tests/integration/bitcoin/block.go
rename to tests/integration/bitcoin/block.go
diff --git a/pkg/tests/integration/domains/domains.go b/tests/integration/domains/domains.go
similarity index 100%
rename from pkg/tests/integration/domains/domains.go
rename to tests/integration/domains/domains.go
diff --git a/pkg/tests/integration/integration_test.go b/tests/integration/integration_test.go
similarity index 72%
rename from pkg/tests/integration/integration_test.go
rename to tests/integration/integration_test.go
index 28b6d6b..218e490 100755
--- a/pkg/tests/integration/integration_test.go
+++ b/tests/integration/integration_test.go
@@ -5,9 +5,9 @@ package integration
 import (
 	"github.com/spf13/viper"
 	"github.com/trustwallet/blockatlas/config"
-	"github.com/trustwallet/blockatlas/pkg/tests/integration/bitcoin"
-	"github.com/trustwallet/blockatlas/pkg/tests/integration/domains"
-	"github.com/trustwallet/blockatlas/pkg/tests/integration/ontology"
+	"github.com/trustwallet/blockatlas/tests/integration/bitcoin"
+	"github.com/trustwallet/blockatlas/tests/integration/domains"
+	"github.com/trustwallet/blockatlas/tests/integration/ontology"
 
 	"github.com/trustwallet/blockatlas/platform"
 	"os"
diff --git a/pkg/tests/integration/ontology/block.go b/tests/integration/ontology/block.go
similarity index 100%
rename from pkg/tests/integration/ontology/block.go
rename to tests/integration/ontology/block.go
diff --git a/pkg/tests/postman/Blockatlas.postman_collection.json b/tests/postman/Blockatlas.postman_collection.json
similarity index 100%
rename from pkg/tests/postman/Blockatlas.postman_collection.json
rename to tests/postman/Blockatlas.postman_collection.json
diff --git a/pkg/tests/postman/collection_data.json b/tests/postman/collection_data.json
similarity index 100%
rename from pkg/tests/postman/collection_data.json
rename to tests/postman/collection_data.json
diff --git a/pkg/tests/postman/domain_data.json b/tests/postman/domain_data.json
similarity index 100%
rename from pkg/tests/postman/domain_data.json
rename to tests/postman/domain_data.json
diff --git a/pkg/tests/postman/healthcheck_data.json b/tests/postman/healthcheck_data.json
similarity index 100%
rename from pkg/tests/postman/healthcheck_data.json
rename to tests/postman/healthcheck_data.json
diff --git a/pkg/tests/postman/market_data.json b/tests/postman/market_data.json
similarity index 100%
rename from pkg/tests/postman/market_data.json
rename to tests/postman/market_data.json
diff --git a/pkg/tests/postman/observer_data.json b/tests/postman/observer_data.json
similarity index 100%
rename from pkg/tests/postman/observer_data.json
rename to tests/postman/observer_data.json
diff --git a/pkg/tests/postman/staking_data.json b/tests/postman/staking_data.json
similarity index 100%
rename from pkg/tests/postman/staking_data.json
rename to tests/postman/staking_data.json
diff --git a/pkg/tests/postman/token_data.json b/tests/postman/token_data.json
similarity index 100%
rename from pkg/tests/postman/token_data.json
rename to tests/postman/token_data.json
diff --git a/pkg/tests/postman/transaction_data.json b/tests/postman/transaction_data.json
similarity index 99%
rename from pkg/tests/postman/transaction_data.json
rename to tests/postman/transaction_data.json
index bbc6084..59945f8 100644
--- a/pkg/tests/postman/transaction_data.json
+++ b/tests/postman/transaction_data.json
@@ -37,7 +37,7 @@
     },
     {
         "handler": "poa",
-        "address": "0x0875BCab22dE3d02402bc38aEe4104e1239374a7"
+        "address": "0x59D82a4B6068188ac06605A8416b5C7D40d6Fc43"
     },
     {
         "handler": "thundertoken",