From 7c61e0486fbafd266c35e1ad9dce1b9a7f1c9361 Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Tue, 7 Sep 2021 15:35:05 +0300
Subject: [PATCH 01/14] Add changes for subscription and merge main.

---
 backend/moonstream/__init__.py                |   7 +
 backend/moonstream/actions.py                 |   9 +-
 backend/moonstream/admin/cli.py               |  14 ++
 .../moonstream/admin/subscription_types.py    |  10 +
 backend/moonstream/data.py                    |   2 +-
 backend/moonstream/middleware.py              |  25 ++-
 backend/moonstream/providers/__init__.py      |  15 +-
 backend/moonstream/providers/bugout.py        |   5 +-
 .../providers/ethereum_blockchain.py          |   3 +-
 backend/moonstream/reporter.py                |  18 ++
 backend/moonstream/routes/address_info.py     |  12 +-
 backend/moonstream/routes/streams.py          | 125 ++++++++-----
 backend/moonstream/routes/subscriptions.py    |  41 +++--
 backend/moonstream/routes/txinfo.py           |   5 +-
 backend/moonstream/routes/users.py            |  37 ++--
 backend/moonstream/settings.py                |   2 +
 backend/requirements.txt                      |   2 +
 backend/sample.env                            |   3 +-
 backend/setup.py                              |  11 +-
 crawlers/ethtxpool/main.go                    |  37 ++--
 crawlers/ethtxpool/sample.env                 |   1 +
 crawlers/mooncrawl/mooncrawl/__init__.py      |   7 +
 crawlers/mooncrawl/mooncrawl/ethcrawler.py    |   2 +-
 crawlers/mooncrawl/mooncrawl/reporter.py      |  18 ++
 crawlers/mooncrawl/mooncrawl/settings.py      |   6 +-
 crawlers/mooncrawl/sample.env                 |   1 +
 crawlers/mooncrawl/setup.py                   |   4 +-
 ..._add_opensea_state_table_and_add_index_.py |  49 +++++
 db/moonstreamdb/models.py                     |  21 ++-
 frontend/pages/index.js                       |  64 ++++---
 frontend/src/AppContext.js                    |  14 +-
 frontend/src/components/Footer.js             |   2 +-
 frontend/src/components/NewSubscription.js    |  93 ++++++++--
 frontend/src/components/Scrollable.js         |   6 +-
 frontend/src/core/hooks/index.js              |   1 -
 frontend/src/core/hooks/useAnalytics.js       |  38 ----
 frontend/src/core/hooks/useLogin.js           |  16 --
 frontend/src/core/hooks/useLogout.js          |   9 +-
 frontend/src/core/hooks/useSignUp.js          |  16 +-
 frontend/src/core/hooks/useToast.js           |  19 +-
 .../providers/AnalyticsProvider/constants.js  |  15 +-
 .../core/providers/AnalyticsProvider/index.js | 174 +++++++++++++++---
 .../src/core/providers/UIProvider/index.js    |  17 +-
 43 files changed, 684 insertions(+), 292 deletions(-)
 create mode 100644 backend/moonstream/reporter.py
 create mode 100644 crawlers/mooncrawl/mooncrawl/reporter.py
 create mode 100644 db/alembic/versions/ecb7817db377_add_opensea_state_table_and_add_index_.py
 delete mode 100644 frontend/src/core/hooks/useAnalytics.js

diff --git a/backend/moonstream/__init__.py b/backend/moonstream/__init__.py
index e69de29b..5df6f535 100644
--- a/backend/moonstream/__init__.py
+++ b/backend/moonstream/__init__.py
@@ -0,0 +1,7 @@
+from .reporter import reporter
+from .version import MOONSTREAM_VERSION
+
+# Reporting
+reporter.tags.append(f"version:{MOONSTREAM_VERSION}")
+reporter.system_report(publish=True)
+reporter.setup_excepthook(publish=True)
diff --git a/backend/moonstream/actions.py b/backend/moonstream/actions.py
index 22612cd8..4ecf8bd4 100644
--- a/backend/moonstream/actions.py
+++ b/backend/moonstream/actions.py
@@ -1,16 +1,18 @@
 import json
 import logging
-from typing import Dict, Any, List, Optional
+from typing import Optional
 from enum import Enum
+
 import boto3  # type: ignore
 from moonstreamdb.models import (
     EthereumAddress,
     EthereumLabel,
 )
 from sqlalchemy import text
-from sqlalchemy.orm import Session, query, query_expression
+from sqlalchemy.orm import Session
 
 from . import data
+from .reporter import reporter
 from .settings import ETHERSCAN_SMARTCONTRACTS_BUCKET
 
 logger = logging.getLogger(__name__)
@@ -46,8 +48,9 @@ def get_contract_source_info(
                     abi=obj_data["ABI"],
                 )
                 return contract_source_info
-            except:
+            except Exception as e:
                 logger.error(f"Failed to load smart contract {object_uri}")
+                reporter.error_report(e)
     return None
 
 
diff --git a/backend/moonstream/admin/cli.py b/backend/moonstream/admin/cli.py
index af24efe8..e95ae61a 100644
--- a/backend/moonstream/admin/cli.py
+++ b/backend/moonstream/admin/cli.py
@@ -71,6 +71,13 @@ This CLI is configured to work with the following API URLs:
         type=str,
         help="Detailed description of the subscription type",
     )
+    parser_subscription_types_create.add_argument(
+        "-c",
+        "--choices",
+        nargs="*",
+        help="Available subscription options for from builder.",
+        required=True,
+    )
     parser_subscription_types_create.add_argument(
         "--icon",
         required=True,
@@ -146,6 +153,13 @@ This CLI is configured to work with the following API URLs:
         type=str,
         help="Detailed description of the subscription type",
     )
+    parser_subscription_types_update.add_argument(
+        "-c",
+        "--choices",
+        nargs="*",
+        help="Available subscription options for form builder.",
+        required=False,
+    )
     parser_subscription_types_update.add_argument(
         "--icon",
         required=False,
diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py
index a44efb1a..0c668910 100644
--- a/backend/moonstream/admin/subscription_types.py
+++ b/backend/moonstream/admin/subscription_types.py
@@ -20,6 +20,7 @@ CANONICAL_SUBSCRIPTION_TYPES = {
     "ethereum_blockchain": SubscriptionTypeResourceData(
         id="ethereum_blockchain",
         name="Ethereum transactions",
+        choices=["input:Address", "tag:nfts"],
         description="Transactions that have been mined into the Ethereum blockchain",
         icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-purple.png",
         stripe_product_id=None,
@@ -30,6 +31,7 @@ CANONICAL_SUBSCRIPTION_TYPES = {
         id="ethereum_whalewatch",
         name="Ethereum whale watch",
         description="Ethereum accounts that have experienced a lot of recent activity",
+        choices=[],
         # Icon taken from: https://www.maxpixel.net/Whale-Cetacean-Wildlife-Symbol-Ocean-Sea-Black-99310
         icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/whalewatch.png",
         stripe_product_id=None,
@@ -40,6 +42,7 @@ CANONICAL_SUBSCRIPTION_TYPES = {
         id="ethereum_txpool",
         name="Ethereum transaction pool",
         description="Transactions that have been submitted into the Ethereum transaction pool but not necessarily mined yet",
+        choices=["input:Address", "tag:nfts"],
         icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-rainbow.png",
         stripe_product_id=None,
         stripe_price_id=None,
@@ -73,6 +76,7 @@ def create_subscription_type(
     id: str,
     name: str,
     description: str,
+    choices: List[str],
     icon_url: str,
     stripe_product_id: Optional[str] = None,
     stripe_price_id: Optional[str] = None,
@@ -134,6 +138,7 @@ def cli_create_subscription_type(args: argparse.Namespace) -> None:
         args.id,
         args.name,
         args.description,
+        args.choices,
         args.icon,
         args.stripe_product_id,
         args.stripe_price_id,
@@ -220,6 +225,7 @@ def update_subscription_type(
     id: str,
     name: Optional[str] = None,
     description: Optional[str] = None,
+    choices: Optional[List[str]] = None,
     icon_url: Optional[str] = None,
     stripe_product_id: Optional[str] = None,
     stripe_price_id: Optional[str] = None,
@@ -254,6 +260,8 @@ def update_subscription_type(
         updated_resource_data["name"] = name
     if description is not None:
         updated_resource_data["description"] = description
+    if choices is not None:
+        updated_resource_data["choices"] = choices
     if icon_url is not None:
         updated_resource_data["icon_url"] = icon_url
     if stripe_product_id is not None:
@@ -295,6 +303,7 @@ def cli_update_subscription_type(args: argparse.Namespace) -> None:
         args.id,
         args.name,
         args.description,
+        args.choices,
         args.icon,
         args.stripe_product_id,
         args.stripe_price_id,
@@ -365,6 +374,7 @@ def ensure_canonical_subscription_types() -> BugoutResources:
                 id,
                 canonical_subscription_type.name,
                 canonical_subscription_type.description,
+                canonical_subscription_type.choices,
                 canonical_subscription_type.icon_url,
                 canonical_subscription_type.stripe_product_id,
                 canonical_subscription_type.stripe_price_id,
diff --git a/backend/moonstream/data.py b/backend/moonstream/data.py
index e8e36c5e..7b7979a7 100644
--- a/backend/moonstream/data.py
+++ b/backend/moonstream/data.py
@@ -10,6 +10,7 @@ class SubscriptionTypeResourceData(BaseModel):
     id: str
     name: str
     description: str
+    choices: Optional[List[str]]
     icon_url: str
     stripe_product_id: Optional[str] = None
     stripe_price_id: Optional[str] = None
@@ -22,7 +23,6 @@ class SubscriptionTypesListResponse(BaseModel):
 
 class SubscriptionResourceData(BaseModel):
     id: str
-    address: str
     color: Optional[str]
     label: Optional[str]
     user_id: str
diff --git a/backend/moonstream/middleware.py b/backend/moonstream/middleware.py
index e2a15175..5e089aa6 100644
--- a/backend/moonstream/middleware.py
+++ b/backend/moonstream/middleware.py
@@ -1,16 +1,36 @@
 import logging
-from typing import Awaitable, Callable, Dict, Optional
+from typing import Any, Awaitable, Callable, Dict, Optional
 
 from bugout.data import BugoutUser
 from bugout.exceptions import BugoutResponseException
+from fastapi import HTTPException, Request, Response
+from starlette.background import BackgroundTask
 from starlette.middleware.base import BaseHTTPMiddleware
-from fastapi import Request, Response
 
+from .reporter import reporter
 from .settings import MOONSTREAM_APPLICATION_ID, bugout_client as bc
 
 logger = logging.getLogger(__name__)
 
 
+class MoonstreamHTTPException(HTTPException):
+    """
+    Extended HTTPException to handle 500 Internal server errors
+    and send crash reports.
+    """
+
+    def __init__(
+        self,
+        status_code: int,
+        detail: Any = None,
+        headers: Optional[Dict[str, Any]] = None,
+        internal_error: Exception = None,
+    ):
+        super().__init__(status_code, detail, headers)
+        if internal_error is not None:
+            reporter.error_report(internal_error)
+
+
 class BroodAuthMiddleware(BaseHTTPMiddleware):
     """
     Checks the authorization header on the request. If it represents a verified Brood user,
@@ -61,6 +81,7 @@ class BroodAuthMiddleware(BaseHTTPMiddleware):
             return Response(status_code=e.status_code, content=e.detail)
         except Exception as e:
             logger.error(f"Error processing Brood response: {str(e)}")
+            reporter.error_report(e)
             return Response(status_code=500, content="Internal server error")
 
         request.state.user = user
diff --git a/backend/moonstream/providers/__init__.py b/backend/moonstream/providers/__init__.py
index fbb2ec7d..f884fe1b 100644
--- a/backend/moonstream/providers/__init__.py
+++ b/backend/moonstream/providers/__init__.py
@@ -39,6 +39,13 @@ from ..stream_queries import StreamQuery
 logger = logging.getLogger(__name__)
 logger.setLevel(logging.WARN)
 
+
+class ReceivingEventsException(Exception):
+    """
+    Raised when error occurs during receiving events from provider.
+    """
+
+
 event_providers: Dict[str, Any] = {
     ethereum_blockchain.event_type: ethereum_blockchain,
     bugout.whalewatch_provider.event_type: bugout.whalewatch_provider,
@@ -91,7 +98,7 @@ def get_events(
                     f"Error receiving events from provider: {provider_name}:\n{repr(e)}"
                 )
             else:
-                raise e
+                raise ReceivingEventsException(e)
 
     events = [event for _, event_list in results.values() for event in event_list]
     if sort_events:
@@ -149,7 +156,7 @@ def latest_events(
                     f"Error receiving events from provider: {provider_name}:\n{repr(e)}"
                 )
             else:
-                raise e
+                raise ReceivingEventsException(e)
 
     events = [event for event_list in results.values() for event in event_list]
     if sort_events:
@@ -202,7 +209,7 @@ def next_event(
                     f"Error receiving events from provider: {provider_name}:\n{repr(e)}"
                 )
             else:
-                raise e
+                raise ReceivingEventsException(e)
 
     event: Optional[data.Event] = None
     for candidate in results.values():
@@ -258,7 +265,7 @@ def previous_event(
                     f"Error receiving events from provider: {provider_name}:\n{repr(e)}"
                 )
             else:
-                raise e
+                raise ReceivingEventsException(e)
 
     event: Optional[data.Event] = None
     for candidate in results.values():
diff --git a/backend/moonstream/providers/bugout.py b/backend/moonstream/providers/bugout.py
index 4ba9629e..5d7ef530 100644
--- a/backend/moonstream/providers/bugout.py
+++ b/backend/moonstream/providers/bugout.py
@@ -19,6 +19,7 @@ from ..stream_queries import StreamQuery
 
 logger = logging.getLogger(__name__)
 logger.setLevel(logging.WARN)
+allowed_tags = ["nfts"]
 
 
 class BugoutEventProviderError(Exception):
@@ -296,9 +297,9 @@ class EthereumTXPoolProvider(BugoutEventProvider):
             for subscription in relevant_subscriptions
         ]
         subscriptions_filters = []
-        for adress in addresses:
+        for address in addresses:
             subscriptions_filters.extend(
-                [f"?#from_address:{adress}", f"?#to_address:{adress}"]
+                [f"?#from_address:{address}", f"?#to_address:{address}"]
             )
 
         return subscriptions_filters
diff --git a/backend/moonstream/providers/ethereum_blockchain.py b/backend/moonstream/providers/ethereum_blockchain.py
index b46e56f4..5a478a84 100644
--- a/backend/moonstream/providers/ethereum_blockchain.py
+++ b/backend/moonstream/providers/ethereum_blockchain.py
@@ -174,7 +174,8 @@ def query_ethereum_transactions(
         EthereumTransaction.value,
         EthereumBlock.timestamp.label("timestamp"),
     ).join(
-        EthereumBlock, EthereumTransaction.block_number == EthereumBlock.block_number
+        EthereumBlock,
+        EthereumTransaction.block_number == EthereumBlock.block_number,
     )
 
     if stream_boundary.include_start:
diff --git a/backend/moonstream/reporter.py b/backend/moonstream/reporter.py
new file mode 100644
index 00000000..1ba0997b
--- /dev/null
+++ b/backend/moonstream/reporter.py
@@ -0,0 +1,18 @@
+import uuid
+
+from humbug.consent import HumbugConsent
+from humbug.report import HumbugReporter
+
+from .settings import HUMBUG_REPORTER_BACKEND_TOKEN
+
+session_id = str(uuid.uuid4())
+client_id = "moonstream-backend"
+
+reporter = HumbugReporter(
+    name="moonstream",
+    consent=HumbugConsent(True),
+    client_id=client_id,
+    session_id=session_id,
+    bugout_token=HUMBUG_REPORTER_BACKEND_TOKEN,
+    tags=[],
+)
diff --git a/backend/moonstream/routes/address_info.py b/backend/moonstream/routes/address_info.py
index 2b95bb92..c985c683 100644
--- a/backend/moonstream/routes/address_info.py
+++ b/backend/moonstream/routes/address_info.py
@@ -3,14 +3,14 @@ from typing import Dict, List, Optional
 
 from sqlalchemy.sql.expression import true
 
-from fastapi import FastAPI, Depends, Query, HTTPException
+from fastapi import FastAPI, Depends, Query
 from fastapi.middleware.cors import CORSMiddleware
 from moonstreamdb.db import yield_db_session
 from sqlalchemy.orm import Session
 
 from .. import actions
 from .. import data
-from ..middleware import BroodAuthMiddleware
+from ..middleware import BroodAuthMiddleware, MoonstreamHTTPException
 from ..settings import DOCS_TARGET_PATH, ORIGINS, DOCS_PATHS
 from ..version import MOONSTREAM_VERSION
 
@@ -73,15 +73,15 @@ async def addresses_labels_bulk_handler(
     about known addresses.
     """
     if limit > 100:
-        raise HTTPException(
+        raise MoonstreamHTTPException(
             status_code=406, detail="The limit cannot exceed 100 addresses"
         )
     try:
         addresses_response = actions.get_address_labels(
             db_session=db_session, start=start, limit=limit, addresses=addresses
         )
-    except Exception as err:
-        logger.error(f"Unable to get info about Ethereum addresses {err}")
-        raise HTTPException(status_code=500)
+    except Exception as e:
+        logger.error(f"Unable to get info about Ethereum addresses {e}")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
 
     return addresses_response
diff --git a/backend/moonstream/routes/streams.py b/backend/moonstream/routes/streams.py
index be46bc58..10d0426c 100644
--- a/backend/moonstream/routes/streams.py
+++ b/backend/moonstream/routes/streams.py
@@ -5,15 +5,16 @@ import logging
 from typing import Dict, List, Optional
 
 from bugout.data import BugoutResource
-from fastapi import FastAPI, HTTPException, Request, Query, Depends
+from fastapi import FastAPI, Request, Query, Depends
 from fastapi.middleware.cors import CORSMiddleware
 from moonstreamdb import db
 from sqlalchemy.orm import Session
 
 
 from .. import data
-from ..middleware import BroodAuthMiddleware
+from ..middleware import BroodAuthMiddleware, MoonstreamHTTPException
 from ..providers import (
+    ReceivingEventsException,
     event_providers,
     get_events,
     latest_events,
@@ -121,17 +122,25 @@ async def stream_handler(
     if q.strip() != "":
         query = stream_queries.parse_query_string(q)
 
-    _, events = get_events(
-        db_session,
-        bc,
-        MOONSTREAM_DATA_JOURNAL_ID,
-        MOONSTREAM_ADMIN_ACCESS_TOKEN,
-        stream_boundary,
-        query,
-        user_subscriptions,
-        result_timeout=10.0,
-        raise_on_error=True,
-    )
+    try:
+        _, events = get_events(
+            db_session,
+            bc,
+            MOONSTREAM_DATA_JOURNAL_ID,
+            MOONSTREAM_ADMIN_ACCESS_TOKEN,
+            stream_boundary,
+            query,
+            user_subscriptions,
+            result_timeout=10.0,
+            raise_on_error=True,
+        )
+    except ReceivingEventsException as e:
+        logger.error("Error receiving events from provider")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
+    except Exception as e:
+        logger.error("Unable to get events")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
+
     response = data.GetEventsResponse(stream_boundary=stream_boundary, events=events)
     return response
 
@@ -155,18 +164,26 @@ async def latest_events_handler(
     if q.strip() != "":
         query = stream_queries.parse_query_string(q)
 
-    events = latest_events(
-        db_session,
-        bc,
-        MOONSTREAM_DATA_JOURNAL_ID,
-        MOONSTREAM_ADMIN_ACCESS_TOKEN,
-        query,
-        1,
-        user_subscriptions,
-        result_timeout=6.0,
-        raise_on_error=True,
-        sort_events=True,
-    )
+    try:
+        events = latest_events(
+            db_session,
+            bc,
+            MOONSTREAM_DATA_JOURNAL_ID,
+            MOONSTREAM_ADMIN_ACCESS_TOKEN,
+            query,
+            1,
+            user_subscriptions,
+            result_timeout=6.0,
+            raise_on_error=True,
+            sort_events=True,
+        )
+    except ReceivingEventsException as e:
+        logger.error("Error receiving events from provider")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
+    except Exception as e:
+        logger.error("Unable to get latest events")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
+
     return events
 
 
@@ -203,17 +220,24 @@ async def next_event_handler(
     if q.strip() != "":
         query = stream_queries.parse_query_string(q)
 
-    event = next_event(
-        db_session,
-        bc,
-        MOONSTREAM_DATA_JOURNAL_ID,
-        MOONSTREAM_ADMIN_ACCESS_TOKEN,
-        stream_boundary,
-        query,
-        user_subscriptions,
-        result_timeout=6.0,
-        raise_on_error=True,
-    )
+    try:
+        event = next_event(
+            db_session,
+            bc,
+            MOONSTREAM_DATA_JOURNAL_ID,
+            MOONSTREAM_ADMIN_ACCESS_TOKEN,
+            stream_boundary,
+            query,
+            user_subscriptions,
+            result_timeout=6.0,
+            raise_on_error=True,
+        )
+    except ReceivingEventsException as e:
+        logger.error("Error receiving events from provider")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
+    except Exception as e:
+        logger.error("Unable to get next events")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
 
     return event
 
@@ -251,16 +275,23 @@ async def previous_event_handler(
     if q.strip() != "":
         query = stream_queries.parse_query_string(q)
 
-    event = previous_event(
-        db_session,
-        bc,
-        MOONSTREAM_DATA_JOURNAL_ID,
-        MOONSTREAM_ADMIN_ACCESS_TOKEN,
-        stream_boundary,
-        query,
-        user_subscriptions,
-        result_timeout=6.0,
-        raise_on_error=True,
-    )
+    try:
+        event = previous_event(
+            db_session,
+            bc,
+            MOONSTREAM_DATA_JOURNAL_ID,
+            MOONSTREAM_ADMIN_ACCESS_TOKEN,
+            stream_boundary,
+            query,
+            user_subscriptions,
+            result_timeout=6.0,
+            raise_on_error=True,
+        )
+    except ReceivingEventsException as e:
+        logger.error("Error receiving events from provider")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
+    except Exception as e:
+        logger.error("Unable to get previous events")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
 
     return event
diff --git a/backend/moonstream/routes/subscriptions.py b/backend/moonstream/routes/subscriptions.py
index 75c854dc..f86f4686 100644
--- a/backend/moonstream/routes/subscriptions.py
+++ b/backend/moonstream/routes/subscriptions.py
@@ -6,12 +6,13 @@ from typing import Dict, List, Optional
 
 from bugout.data import BugoutResource, BugoutResources
 from bugout.exceptions import BugoutResponseException
-from fastapi import FastAPI, HTTPException, Request, Form
+from fastapi import FastAPI, Request, Form
 from fastapi.middleware.cors import CORSMiddleware
 
 from ..admin import subscription_types
 from .. import data
-from ..middleware import BroodAuthMiddleware
+from ..middleware import BroodAuthMiddleware, MoonstreamHTTPException
+from ..reporter import reporter
 from ..settings import (
     DOCS_TARGET_PATH,
     DOCS_PATHS,
@@ -77,7 +78,7 @@ async def add_subscription_handler(
     ]
 
     if subscription_type_id not in available_subscription_type_ids:
-        raise HTTPException(
+        raise MoonstreamHTTPException(
             status_code=404,
             detail=f"Invalid subscription type: {subscription_type_id}.",
         )
@@ -99,10 +100,11 @@ async def add_subscription_handler(
             application_id=MOONSTREAM_APPLICATION_ID,
             resource_data=resource_data,
         )
+    except BugoutResponseException as e:
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
-        logger.error("Error creating subscription resource:")
-        logger.error(e)
-        raise HTTPException(status_code=500)
+        logger.error(f"Error creating subscription resource: {str(e)}")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
 
     return data.SubscriptionResourceData(
         id=str(resource.id),
@@ -123,14 +125,14 @@ async def delete_subscription_handler(request: Request, subscription_id: str):
     """
     Delete subscriptions.
     """
-
     token = request.state.token
     try:
         deleted_resource = bc.delete_resource(token=token, resource_id=subscription_id)
     except BugoutResponseException as e:
-        raise HTTPException(status_code=e.status_code, detail=e.detail)
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
-        raise HTTPException(status_code=500)
+        logger.error(f"Error deleting subscription: {str(e)}")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
 
     return data.SubscriptionResourceData(
         id=str(deleted_resource.id),
@@ -154,12 +156,14 @@ async def get_subscriptions_handler(request: Request) -> data.SubscriptionsListR
     }
     try:
         resources: BugoutResources = bc.list_resources(token=token, params=params)
+    except BugoutResponseException as e:
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
         logger.error(
-            f"Error listing subscriptions for user ({request.user.id}) with token ({request.state.token})"
+            f"Error listing subscriptions for user ({request.user.id}) with token ({request.state.token}), error: {str(e)}"
         )
-        logger.error(e)
-        raise HTTPException(status_code=500)
+        reporter.error_report(e)
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
 
     return data.SubscriptionsListResponse(
         subscriptions=[
@@ -190,7 +194,6 @@ async def update_subscriptions_handler(
     """
     Get user's subscriptions.
     """
-
     token = request.state.token
 
     update = {}
@@ -210,9 +213,10 @@ async def update_subscriptions_handler(
             ).dict(),
         )
     except BugoutResponseException as e:
-        raise HTTPException(status_code=e.status_code, detail=e.detail)
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
-        raise HTTPException(status_code=500)
+        logger.error(f"Error getting user subscriptions: {str(e)}")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
 
     return data.SubscriptionResourceData(
         id=str(resource.id),
@@ -238,9 +242,10 @@ async def list_subscription_types() -> data.SubscriptionTypesListResponse:
             data.SubscriptionTypeResourceData.validate(resource.resource_data)
             for resource in response.resources
         ]
+    except BugoutResponseException as e:
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
-        logger.error("Error reading subscription types from Brood API:")
-        logger.error(e)
-        raise HTTPException(status_code=500)
+        logger.error(f"Error reading subscription types from Brood API: {str(e)}")
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
 
     return data.SubscriptionTypesListResponse(subscription_types=results)
diff --git a/backend/moonstream/routes/txinfo.py b/backend/moonstream/routes/txinfo.py
index 20eec3c7..8ab9a5ec 100644
--- a/backend/moonstream/routes/txinfo.py
+++ b/backend/moonstream/routes/txinfo.py
@@ -6,9 +6,7 @@ transactions, etc.) with side information and return objects that are better sui
 end users.
 """
 import logging
-from typing import Dict, Optional
-
-from sqlalchemy.sql.expression import true
+from typing import Dict
 
 from fastapi import FastAPI, Depends
 from fastapi.middleware.cors import CORSMiddleware
@@ -54,6 +52,7 @@ app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths)
 
 # TODO(zomglings): Factor out the enrichment logic into a separate action, because it may be useful
 # independently from serving API calls (e.g. data processing).
+# TODO(kompotkot): Re-organize function to be able handle each steps with exceptions.
 @app.post(
     "/ethereum_blockchain",
     tags=["txinfo"],
diff --git a/backend/moonstream/routes/users.py b/backend/moonstream/routes/users.py
index 5101a25c..4407a67e 100644
--- a/backend/moonstream/routes/users.py
+++ b/backend/moonstream/routes/users.py
@@ -7,15 +7,10 @@ import uuid
 
 from bugout.data import BugoutToken, BugoutUser
 from bugout.exceptions import BugoutResponseException
-from fastapi import (
-    FastAPI,
-    Form,
-    HTTPException,
-    Request,
-)
+from fastapi import FastAPI, Form, Request
 from fastapi.middleware.cors import CORSMiddleware
 
-from ..middleware import BroodAuthMiddleware
+from ..middleware import BroodAuthMiddleware, MoonstreamHTTPException
 from ..settings import (
     MOONSTREAM_APPLICATION_ID,
     DOCS_TARGET_PATH,
@@ -75,9 +70,9 @@ async def create_user_handler(
             application_id=MOONSTREAM_APPLICATION_ID,
         )
     except BugoutResponseException as e:
-        raise HTTPException(status_code=e.status_code, detail=e.detail)
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
-        raise HTTPException(status_code=500)
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
     return user
 
 
@@ -92,9 +87,9 @@ async def restore_password_handler(email: str = Form(...)) -> Dict[str, Any]:
     try:
         response = bc.restore_password(email=email)
     except BugoutResponseException as e:
-        raise HTTPException(status_code=e.status_code, detail=e.detail)
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
-        raise HTTPException(status_code=500)
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
     return response
 
 
@@ -105,9 +100,9 @@ async def reset_password_handler(
     try:
         response = bc.reset_password(reset_id=reset_id, new_password=new_password)
     except BugoutResponseException as e:
-        raise HTTPException(status_code=e.status_code, detail=e.detail)
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
-        raise HTTPException(status_code=500)
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
     return response
 
 
@@ -121,9 +116,9 @@ async def change_password_handler(
             token=token, current_password=current_password, new_password=new_password
         )
     except BugoutResponseException as e:
-        raise HTTPException(status_code=e.status_code, detail=e.detail)
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
-        raise HTTPException(status_code=500)
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
     return user
 
 
@@ -136,9 +131,9 @@ async def delete_user_handler(
     try:
         user = bc.delete_user(token=token, user_id=user.id, password=password)
     except BugoutResponseException as e:
-        raise HTTPException(status_code=e.status_code, detail=e.detail)
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
-        raise HTTPException(status_code=500)
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
     return user
 
 
@@ -153,11 +148,11 @@ async def login_handler(
             application_id=MOONSTREAM_APPLICATION_ID,
         )
     except BugoutResponseException as e:
-        raise HTTPException(
+        raise MoonstreamHTTPException(
             status_code=e.status_code, detail=f"Error from Brood API: {e.detail}"
         )
     except Exception as e:
-        raise HTTPException(status_code=500)
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
     return token
 
 
@@ -167,7 +162,7 @@ async def logout_handler(request: Request) -> uuid.UUID:
     try:
         token_id: uuid.UUID = bc.revoke_token(token=token)
     except BugoutResponseException as e:
-        raise HTTPException(status_code=e.status_code, detail=e.detail)
+        raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
     except Exception as e:
-        raise HTTPException(status_code=500)
+        raise MoonstreamHTTPException(status_code=500, internal_error=e)
     return token_id
diff --git a/backend/moonstream/settings.py b/backend/moonstream/settings.py
index 6408b1d8..0a90d751 100644
--- a/backend/moonstream/settings.py
+++ b/backend/moonstream/settings.py
@@ -9,6 +9,8 @@ bugout_client = Bugout(brood_api_url=BUGOUT_BROOD_URL, spire_api_url=BUGOUT_SPIR
 
 BUGOUT_REQUEST_TIMEOUT_SECONDS = 5
 
+HUMBUG_REPORTER_BACKEND_TOKEN = os.environ.get("HUMBUG_REPORTER_BACKEND_TOKEN")
+
 # Default value is "" instead of None so that mypy understands that MOONSTREAM_APPLICATION_ID is a string
 MOONSTREAM_APPLICATION_ID = os.environ.get("MOONSTREAM_APPLICATION_ID", "")
 if MOONSTREAM_APPLICATION_ID == "":
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 44e49539..4cc7c386 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -11,6 +11,7 @@ fastapi==0.66.0
 h11==0.12.0
 idna==3.2
 jmespath==0.10.0
+humbug==0.2.7
 -e git+https://git@github.com/bugout-dev/moonstream.git@94135b054cabb9dc11b0a2406431619279979469#egg=moonstreamdb&subdirectory=db
 mypy==0.910
 mypy-extensions==0.4.3
@@ -28,5 +29,6 @@ toml==0.10.2
 tomli==1.0.4
 types-python-dateutil==0.1.6
 typing-extensions==3.10.0.0
+types-requests==2.25.6
 urllib3==1.26.6
 uvicorn==0.14.0
diff --git a/backend/sample.env b/backend/sample.env
index 054e040d..779bbb0c 100644
--- a/backend/sample.env
+++ b/backend/sample.env
@@ -5,6 +5,7 @@ export MOONSTREAM_DATA_JOURNAL_ID="<bugout_journal_id_to_store_blockchain_data>"
 export MOONSTREAM_DB_URI="postgresql://<username>:<password>@<db_host>:<db_port>/<db_name>"
 export MOONSTREAM_POOL_SIZE=0
 export MOONSTREAM_ADMIN_ACCESS_TOKEN="<Access token to application resources>"
-export AWS_S3_SMARTCONTRACT_BUCKET=""
+export AWS_S3_SMARTCONTRACT_BUCKET="<AWS S3 bucket to store smart contracts>"
 export BUGOUT_BROOD_URL="https://auth.bugout.dev"
 export BUGOUT_SPIRE_URL="https://spire.bugout.dev"
+export HUMBUG_REPORTER_BACKEND_TOKEN="<Bugout Humbug token for crash reports>"
diff --git a/backend/setup.py b/backend/setup.py
index 0cd9b6e5..06a2a807 100644
--- a/backend/setup.py
+++ b/backend/setup.py
@@ -10,7 +10,16 @@ setup(
     name="moonstream",
     version=MOONSTREAM_VERSION,
     packages=find_packages(),
-    install_requires=["boto3", "bugout >= 0.1.17", "fastapi", "python-dateutil", "uvicorn", "types-python-dateutil"],
+    install_requires=[
+        "boto3",
+        "bugout >= 0.1.17",
+        "fastapi",
+        "humbug>=0.2.7",
+        "python-dateutil",
+        "uvicorn",
+        "types-python-dateutil",
+        "types-requests",
+    ],
     extras_require={
         "dev": ["black", "mypy"],
         "distribute": ["setuptools", "twine", "wheel"],
diff --git a/crawlers/ethtxpool/main.go b/crawlers/ethtxpool/main.go
index 224ae7dc..e4aa4f55 100644
--- a/crawlers/ethtxpool/main.go
+++ b/crawlers/ethtxpool/main.go
@@ -10,23 +10,20 @@ import (
 	"encoding/json"
 	"flag"
 	"fmt"
+	"math/big"
 	"os"
 	"time"
 
 	humbug "github.com/bugout-dev/humbug/go/pkg"
 	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/common/hexutil"
-
+	"github.com/ethereum/go-ethereum/params"
 	"github.com/ethereum/go-ethereum/rpc"
 	"github.com/google/uuid"
 )
 
-// Generate humbug client to be able write data in Bugout journal.
-func humbugClientFromEnv() (*humbug.HumbugReporter, error) {
-	clientID := os.Getenv("ETHTXPOOL_HUMBUG_CLIENT_ID")
-	humbugToken := os.Getenv("ETHTXPOOL_HUMBUG_TOKEN")
-	sessionID := uuid.New().String()
-
+// Generate humbug client
+func humbugClient(sessionID string, clientID string, humbugToken string) (*humbug.HumbugReporter, error) {
 	consent := humbug.CreateHumbugConsent(humbug.True)
 	reporter, err := humbug.CreateHumbugReporter(consent, clientID, sessionID, humbugToken)
 	return reporter, err
@@ -124,11 +121,6 @@ func PollTxpoolContent(gethClient *rpc.Client, interval int, reporter *humbug.Hu
 						continue
 					}
 
-					// TODO(kompotkot, zomglings): Humbug API (on Spire) support bulk publication of reports. We should modify
-					// Humbug go client to use the bulk publish endpoint. Currently, if we have to publish all transactions
-					// pending in txpool, we *will* get rate limited. We may want to consider adding a publisher to the
-					// Humbug go client that can listen on a channel and will handle rate limiting, bulk publication etc. itself
-					// (without user having to worry about it).
 					ReportTitle := "Ethereum: Pending transaction: " + transactionHash.String()
 					ReportTags := []string{
 						"hash:" + transactionHash.String(),
@@ -138,6 +130,7 @@ func PollTxpoolContent(gethClient *rpc.Client, interval int, reporter *humbug.Hu
 						fmt.Sprintf("max_priority_fee_per_gas:%d", pendingTx.Transaction.MaxPriorityFeePerGas.ToInt()),
 						fmt.Sprintf("max_fee_per_gas:%d", pendingTx.Transaction.MaxFeePerGas.ToInt()),
 						fmt.Sprintf("gas:%d", pendingTx.Transaction.Gas),
+						fmt.Sprintf("value:%d", new(big.Float).Quo(new(big.Float).SetInt(transaction.Value.ToInt()), big.NewFloat(params.Ether))),
 						"crawl_type:ethereum_txpool",
 					}
 					report := humbug.Report{
@@ -188,6 +181,23 @@ func main() {
 	flag.IntVar(&intervalSeconds, "interval", 1, "Number of seconds to wait between RPC calls to query the transaction pool (default: 1)")
 	flag.Parse()
 
+	sessionID := uuid.New().String()
+	
+	// Humbug crash client to collect errors
+	crashReporter, err := humbugClient(sessionID, "moonstream-crawlers", os.Getenv("HUMBUG_REPORTER_CRAWLERS_TOKEN"))
+	if err != nil {
+		panic(fmt.Sprintf("Invalid Humbug Crash configuration: %s", err.Error()))
+	}
+	crashReporter.Publish(humbug.SystemReport())
+
+	defer func() {
+		message := recover()
+		if message != nil {
+			fmt.Printf("Error: %s\n", message)
+			crashReporter.Publish(humbug.PanicReport(message))
+		}
+	}()
+
 	// Set connection with Ethereum blockchain via geth
 	gethClient, err := rpc.Dial(gethConnectionString)
 	if err != nil {
@@ -195,7 +205,8 @@ func main() {
 	}
 	defer gethClient.Close()
 
-	reporter, err := humbugClientFromEnv()
+	// Humbug client to be able write data in Bugout journal
+	reporter, err := humbugClient(sessionID, os.Getenv("ETHTXPOOL_HUMBUG_CLIENT_ID"), os.Getenv("ETHTXPOOL_HUMBUG_TOKEN"))
 	if err != nil {
 		panic(fmt.Sprintf("Invalid Humbug configuration: %s", err.Error()))
 	}
diff --git a/crawlers/ethtxpool/sample.env b/crawlers/ethtxpool/sample.env
index abd4bc8e..809cc325 100644
--- a/crawlers/ethtxpool/sample.env
+++ b/crawlers/ethtxpool/sample.env
@@ -1,2 +1,3 @@
 export ETHTXPOOL_HUMBUG_CLIENT_ID="<client id for the crawling machine>"
 export ETHTXPOOL_HUMBUG_TOKEN="<Generate an integration and a Humbug token from https://bugout.dev/account/teams>"
+export HUMBUG_REPORTER_CRAWLERS_TOKEN="<Bugout Humbug token for crash reports>"
diff --git a/crawlers/mooncrawl/mooncrawl/__init__.py b/crawlers/mooncrawl/mooncrawl/__init__.py
index e69de29b..9548e297 100644
--- a/crawlers/mooncrawl/mooncrawl/__init__.py
+++ b/crawlers/mooncrawl/mooncrawl/__init__.py
@@ -0,0 +1,7 @@
+from .reporter import reporter
+from .version import MOONCRAWL_VERSION
+
+# Reporting
+reporter.tags.append(f"version:{MOONCRAWL_VERSION}")
+reporter.system_report(publish=True)
+reporter.setup_excepthook(publish=True)
diff --git a/crawlers/mooncrawl/mooncrawl/ethcrawler.py b/crawlers/mooncrawl/mooncrawl/ethcrawler.py
index 793ab3ea..5e9275c4 100644
--- a/crawlers/mooncrawl/mooncrawl/ethcrawler.py
+++ b/crawlers/mooncrawl/mooncrawl/ethcrawler.py
@@ -48,7 +48,7 @@ def yield_blocks_numbers_lists(
         print(
             "Wrong format provided, expected {bottom_block}-{top_block}, as ex. 105-340"
         )
-        return
+        raise Exception
 
     starting_block = max(input_start_block, input_end_block)
     ending_block = min(input_start_block, input_end_block)
diff --git a/crawlers/mooncrawl/mooncrawl/reporter.py b/crawlers/mooncrawl/mooncrawl/reporter.py
new file mode 100644
index 00000000..0cf170ac
--- /dev/null
+++ b/crawlers/mooncrawl/mooncrawl/reporter.py
@@ -0,0 +1,18 @@
+import uuid
+
+from humbug.consent import HumbugConsent
+from humbug.report import HumbugReporter
+
+from .settings import HUMBUG_REPORTER_CRAWLERS_TOKEN
+
+session_id = str(uuid.uuid4())
+client_id = "moonstream-crawlers"
+
+reporter = HumbugReporter(
+    name="moonstream-crawlers",
+    consent=HumbugConsent(True),
+    client_id=client_id,
+    session_id=session_id,
+    bugout_token=HUMBUG_REPORTER_CRAWLERS_TOKEN,
+    tags=[],
+)
diff --git a/crawlers/mooncrawl/mooncrawl/settings.py b/crawlers/mooncrawl/mooncrawl/settings.py
index 82b13772..307f2e8a 100644
--- a/crawlers/mooncrawl/mooncrawl/settings.py
+++ b/crawlers/mooncrawl/mooncrawl/settings.py
@@ -1,5 +1,9 @@
 import os
 
+# Bugout
+HUMBUG_REPORTER_CRAWLERS_TOKEN = os.environ.get("HUMBUG_REPORTER_CRAWLERS_TOKEN")
+
+# Geth
 MOONSTREAM_IPC_PATH = os.environ.get("MOONSTREAM_IPC_PATH", None)
 
 MOONSTREAM_CRAWL_WORKERS = 4
@@ -12,5 +16,5 @@ except:
         f"Could not parse MOONSTREAM_CRAWL_WORKERS as int: {MOONSTREAM_CRAWL_WORKERS_RAW}"
     )
 
-
+# Etherscan
 MOONSTREAM_ETHERSCAN_TOKEN = os.environ.get("MOONSTREAM_ETHERSCAN_TOKEN")
diff --git a/crawlers/mooncrawl/sample.env b/crawlers/mooncrawl/sample.env
index 5ebad6e0..5d4ac548 100644
--- a/crawlers/mooncrawl/sample.env
+++ b/crawlers/mooncrawl/sample.env
@@ -6,3 +6,4 @@ export MOONSTREAM_ETHERSCAN_TOKEN="<Token for etherscan>"
 export AWS_S3_SMARTCONTRACT_BUCKET="<AWS S3 bucket for smart contracts>"
 export MOONSTREAM_HUMBUG_TOKEN="<Token for crawlers store data via Humbug>"
 export COINMARKETCAP_API_KEY="<API key to parse conmarketcap>"
+export HUMBUG_REPORTER_CRAWLERS_TOKEN="<Bugout Humbug token for crash reports>"
diff --git a/crawlers/mooncrawl/setup.py b/crawlers/mooncrawl/setup.py
index 1e893bd4..f26b82d5 100644
--- a/crawlers/mooncrawl/setup.py
+++ b/crawlers/mooncrawl/setup.py
@@ -1,6 +1,5 @@
 from setuptools import find_packages, setup
 
-from mooncrawl.version import MOONCRAWL_VERSION
 
 long_description = ""
 with open("README.md") as ifp:
@@ -8,7 +7,7 @@ with open("README.md") as ifp:
 
 setup(
     name="mooncrawl",
-    version=MOONCRAWL_VERSION,
+    version="0.0.3",
     author="Bugout.dev",
     author_email="engineers@bugout.dev",
     license="Apache License 2.0",
@@ -34,6 +33,7 @@ setup(
     zip_safe=False,
     install_requires=[
         "moonstreamdb @ git+https://git@github.com/bugout-dev/moonstream.git@39d2b8e36a49958a9ae085ec2cc1be3fc732b9d0#egg=moonstreamdb&subdirectory=db",
+        "humbug",
         "python-dateutil",
         "requests",
         "tqdm",
diff --git a/db/alembic/versions/ecb7817db377_add_opensea_state_table_and_add_index_.py b/db/alembic/versions/ecb7817db377_add_opensea_state_table_and_add_index_.py
new file mode 100644
index 00000000..98a07eb0
--- /dev/null
+++ b/db/alembic/versions/ecb7817db377_add_opensea_state_table_and_add_index_.py
@@ -0,0 +1,49 @@
+"""Add opensea state table and add index by label_data ->> name
+
+Revision ID: ecb7817db377
+Revises: ea8185bd24c7
+Create Date: 2021-08-31 17:44:24.139028
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = "ecb7817db377"
+down_revision = "ea8185bd24c7"
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table(
+        "opensea_crawler_state",
+        sa.Column("id", sa.Integer(), nullable=False),
+        sa.Column("query", sa.Text(), nullable=False),
+        sa.Column(
+            "crawled_at",
+            sa.DateTime(timezone=True),
+            server_default=sa.text("TIMEZONE('utc', statement_timestamp())"),
+            nullable=False,
+        ),
+        sa.Column("total_count", sa.Integer(), nullable=False),
+        sa.PrimaryKeyConstraint("id", name=op.f("pk_opensea_crawler_state")),
+        sa.UniqueConstraint("id", name=op.f("uq_opensea_crawler_state_id")),
+    )
+    op.execute(
+        "ALTER TABLE ethereum_labels DROP CONSTRAINT IF EXISTS uq_ethereum_labels_label"
+    )
+
+    op.execute(
+        f"CREATE INDEX idx_ethereum_labels_opensea_nft_name ON ethereum_labels((label_data->>'name')) where label='opensea_nft';"
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table("opensea_crawler_state")
+    op.drop_index("idx_ethereum_labels_opensea_nft_name")
+    # ### end Alembic commands ###
diff --git a/db/moonstreamdb/models.py b/db/moonstreamdb/models.py
index 3114133f..7d7798a4 100644
--- a/db/moonstreamdb/models.py
+++ b/db/moonstreamdb/models.py
@@ -11,7 +11,6 @@ from sqlalchemy import (
     Numeric,
     Text,
     VARCHAR,
-    UniqueConstraint,
 )
 from sqlalchemy.dialects.postgresql import JSONB, UUID
 from sqlalchemy.sql import expression
@@ -136,7 +135,6 @@ class EthereumLabel(Base):  # type: ignore
     """
 
     __tablename__ = "ethereum_labels"
-    __table_args__ = (UniqueConstraint("label", "address_id"),)
 
     id = Column(
         UUID(as_uuid=True),
@@ -212,3 +210,22 @@ class ESDEventSignature(Base):  # type: ignore
     created_at = Column(
         DateTime(timezone=True), server_default=utcnow(), nullable=False
     )
+
+
+class OpenSeaCrawlingState(Base):  # type: ignore
+    """
+    Model for control opeansea crawling state.
+    """
+
+    __tablename__ = "opensea_crawler_state"
+
+    id = Column(Integer, primary_key=True, unique=True, nullable=False)
+    query = Column(Text, nullable=False)
+    crawled_at = Column(
+        DateTime(timezone=True),
+        server_default=utcnow(),
+        onupdate=utcnow(),
+        nullable=False,
+    )
+
+    total_count = Column(Integer, nullable=False)
diff --git a/frontend/pages/index.js b/frontend/pages/index.js
index c3e21079..0bcfdeaa 100644
--- a/frontend/pages/index.js
+++ b/frontend/pages/index.js
@@ -24,12 +24,15 @@ import {
 } from "@chakra-ui/react";
 import dynamic from "next/dynamic";
 import useUser from "../src/core/hooks/useUser";
-import useAnalytics from "../src/core/hooks/useAnalytics";
 import useModals from "../src/core/hooks/useModals";
 import useRouter from "../src/core/hooks/useRouter";
-import { MIXPANEL_PROPS } from "../src/core/providers/AnalyticsProvider/constants";
+import {
+  MIXPANEL_PROPS,
+  MIXPANEL_EVENTS,
+} from "../src/core/providers/AnalyticsProvider/constants";
 import UIContext from "../src/core/providers/UIProvider/context";
 import { AWS_ASSETS_PATH } from "../src/core/constants";
+import mixpanel from "mixpanel-browser";
 const SplitWithImage = dynamic(
   () => import("../src/components/SplitWithImage"),
   {
@@ -105,7 +108,6 @@ const Homepage = () => {
 
   const router = useRouter();
   const { isInit } = useUser();
-  const { MIXPANEL_EVENTS, track } = useAnalytics();
   const { toggleModal } = useModals();
   const [
     isLargerThan720px,
@@ -379,27 +381,30 @@ const Homepage = () => {
                       label: "Crypto trader",
                       link: "/#cryptoTrader",
                       onClick: () => {
-                        track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
-                          [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to CryptoTrader`,
-                        });
+                        mixpanel.get_distinct_id() &&
+                          mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
+                            [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `scroll to CryptoTrader`,
+                          });
                       },
                     }}
                     button2={{
                       label: "Algorithmic Fund",
                       link: "/#algoFund",
                       onClick: () => {
-                        track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
-                          [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to AlgoFund`,
-                        });
+                        mixpanel.get_distinct_id() &&
+                          mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
+                            [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `scroll to AlgoFund`,
+                          });
                       },
                     }}
                     button3={{
                       label: "Developer",
                       link: "/#smartDeveloper",
                       onClick: () => {
-                        track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
-                          [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to Developer`,
-                        });
+                        mixpanel.get_distinct_id() &&
+                          mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
+                            [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `scroll to Developer`,
+                          });
                       },
                     }}
                   />
@@ -417,9 +422,10 @@ const Homepage = () => {
                   cta={{
                     label: "I want early access!",
                     onClick: () => {
-                      track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
-                        [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: Crypto trader`,
-                      });
+                      mixpanel.get_distinct_id() &&
+                        mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
+                          [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `Early access CTA: Crypto trader`,
+                        });
                       toggleModal("hubspot-trader");
                     },
                   }}
@@ -464,9 +470,10 @@ const Homepage = () => {
                   cta={{
                     label: "I want early access!",
                     onClick: () => {
-                      track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
-                        [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: Algo fund`,
-                      });
+                      mixpanel.get_distinct_id() &&
+                        mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
+                          [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `Early access CTA: Algo fund`,
+                        });
                       toggleModal("hubspot-fund");
                     },
                   }}
@@ -509,9 +516,10 @@ const Homepage = () => {
                   cta={{
                     label: "I want early access!",
                     onClick: () => {
-                      track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
-                        [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: developer`,
-                      });
+                      mixpanel.get_distinct_id() &&
+                        mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
+                          [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `Early access CTA: developer`,
+                        });
                       toggleModal("hubspot-developer");
                     },
                   }}
@@ -520,9 +528,10 @@ const Homepage = () => {
                     network: "github",
                     label: "See our github",
                     onClick: () => {
-                      track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
-                        [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Github link in landing page`,
-                      });
+                      mixpanel.get_distinct_id() &&
+                        mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
+                          [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `Github link in landing page`,
+                        });
                     },
                   }}
                   elementName={"element3"}
@@ -568,9 +577,10 @@ const Homepage = () => {
                     colorScheme="suggested"
                     id="test"
                     onClick={() => {
-                      track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
-                        [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Join our discord`,
-                      });
+                      mixpanel.get_distinct_id() &&
+                        mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
+                          [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `Join our discord`,
+                        });
                       toggleModal("hubspot");
                     }}
                   >
diff --git a/frontend/src/AppContext.js b/frontend/src/AppContext.js
index 7a238760..f2c56e74 100644
--- a/frontend/src/AppContext.js
+++ b/frontend/src/AppContext.js
@@ -13,13 +13,13 @@ const AppContext = (props) => {
   return (
     <UserProvider>
       <ModalProvider>
-        <AnalyticsProvider>
-          <StripeProvider>
-            <ChakraProvider theme={theme}>
-              <UIProvider>{props.children}</UIProvider>
-            </ChakraProvider>
-          </StripeProvider>
-        </AnalyticsProvider>
+        <StripeProvider>
+          <ChakraProvider theme={theme}>
+            <UIProvider>
+              <AnalyticsProvider>{props.children}</AnalyticsProvider>
+            </UIProvider>
+          </ChakraProvider>
+        </StripeProvider>
       </ModalProvider>
     </UserProvider>
   );
diff --git a/frontend/src/components/Footer.js b/frontend/src/components/Footer.js
index d212befe..2b95a9f6 100644
--- a/frontend/src/components/Footer.js
+++ b/frontend/src/components/Footer.js
@@ -6,7 +6,7 @@ import RouterLink from "next/link";
 const ICONS = [
   {
     social: "discord",
-    link: "https://discord.gg/FetK5BxD",
+    link: "https://discord.gg/K56VNUQGvA",
   },
 
   { social: "twit", link: "https://twitter.com/moonstreamto" },
diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js
index 230ea755..284e3c3b 100644
--- a/frontend/src/components/NewSubscription.js
+++ b/frontend/src/components/NewSubscription.js
@@ -35,14 +35,35 @@ const _NewSubscription = ({
   const [radioState, setRadioState] = useState(
     initialType ?? "ethereum_blockchain"
   );
+
+  const [subscriptionAdressFormatRadio, setsubscriptionAdressFormatRadio] =
+    useState("");
+
   let { getRootProps, getRadioProps } = useRadioGroup({
     name: "type",
     defaultValue: radioState,
     onChange: setRadioState,
   });
 
+  let {
+    getRootProps: getRootPropsSubscription,
+    getRadioProps: getRadioPropsSubscription,
+  } = useRadioGroup({
+    name: "subscription",
+    onChange: setsubscriptionAdressFormatRadio,
+  });
+
+  console.log(
+    useRadioGroup({
+      name: "subscription1",
+      onChange: setsubscriptionAdressFormatRadio,
+    })
+  );
+
   const group = getRootProps();
 
+  const subscriptionAddressTypeGroup = getRootPropsSubscription();
+
   useEffect(() => {
     if (setIsLoading) {
       setIsLoading(createSubscription.isLoading);
@@ -57,6 +78,13 @@ const _NewSubscription = ({
 
   const createSubscriptionWrapper = useCallback(
     (props) => {
+      if (
+        subscriptionAdressFormatRadio.startsWith("tags") &&
+        radioState != "ethereum_whalewatch"
+      ) {
+        props.address = subscriptionAdressFormatRadio.split(":")[1];
+      }
+
       createSubscription.mutate({
         ...props,
         color: color,
@@ -68,6 +96,16 @@ const _NewSubscription = ({
 
   if (typesCache.isLoading) return <Spinner />;
 
+  function search(nameKey, myArray) {
+    for (var i = 0; i < myArray.length; i++) {
+      if (myArray[i].id === nameKey) {
+        return myArray[i];
+      }
+    }
+  }
+
+  console.log(radioState);
+
   const handleChangeColorComplete = (color) => {
     setColor(color.hex);
   };
@@ -89,19 +127,6 @@ const _NewSubscription = ({
           {errors?.label && errors?.label.message}
         </FormErrorMessage>
       </FormControl>
-      <FormControl isInvalid={errors?.address}>
-        <Input
-          type="text"
-          autoComplete="off"
-          my={2}
-          placeholder="Address to subscribe to"
-          name="address"
-          ref={register({ required: "address is required!" })}
-        ></Input>
-        <FormErrorMessage color="unsafe.400" pl="1">
-          {errors?.address && errors?.address.message}
-        </FormErrorMessage>
-      </FormControl>
       <Stack my={4} direction="column">
         {/* <Text fontWeight="600">
           {isFreeOption
@@ -119,6 +144,8 @@ const _NewSubscription = ({
                   !type.active ||
                   (isFreeOption && type.id !== "ethereum_blockchain"),
               });
+              console.log(type);
+
               return (
                 <RadioCard key={`subscription_type_${type.id}`} {...radio}>
                   {type.name}
@@ -126,6 +153,46 @@ const _NewSubscription = ({
               );
             })}
           </HStack>
+          <Stack my={4} direction="column">
+            <FormControl isInvalid={errors?.subscription_type}>
+              <HStack {...subscriptionAddressTypeGroup} alignItems="stretch">
+                {search(radioState, typesCache.data).choices.map(
+                  (addition_selects) => {
+                    const radio = getRadioPropsSubscription({
+                      value: addition_selects,
+                    });
+                    console.log(radio);
+                    return (
+                      <RadioCard
+                        key={`subscription_tags_${addition_selects}`}
+                        {...radio}
+                      >
+                        {addition_selects.split(":")[1]}
+                      </RadioCard>
+                    );
+                    // }
+                  }
+                )}
+              </HStack>
+            </FormControl>
+          </Stack>
+
+          {subscriptionAdressFormatRadio.startsWith("input") &&
+            radioState != "ethereum_whalewatch" && (
+              <FormControl isInvalid={errors?.address}>
+                <Input
+                  type="text"
+                  autoComplete="off"
+                  my={2}
+                  placeholder="Address to subscribe to"
+                  name="address"
+                  ref={register({ required: "address is required!" })}
+                ></Input>
+                <FormErrorMessage color="unsafe.400" pl="1">
+                  {errors?.address && errors?.address.message}
+                </FormErrorMessage>
+              </FormControl>
+            )}
           <Input
             type="hidden"
             placeholder="subscription_type"
diff --git a/frontend/src/components/Scrollable.js b/frontend/src/components/Scrollable.js
index 4ab9f6a7..bdcdf0f0 100644
--- a/frontend/src/components/Scrollable.js
+++ b/frontend/src/components/Scrollable.js
@@ -1,13 +1,13 @@
 import { Flex, Box } from "@chakra-ui/react";
 import React, { useEffect, useRef, useState } from "react";
-import { useRouter, useAnalytics } from "../core/hooks";
+import { useRouter } from "../core/hooks";
+import mixpanel from "mixpanel-browser";
 const Scrollable = (props) => {
   const scrollerRef = useRef();
   const router = useRouter();
   const [path, setPath] = useState();
 
   const [scrollDepth, setScrollDepth] = useState(0);
-  const { mixpanel, isLoaded } = useAnalytics();
 
   const getScrollPrecent = ({ currentTarget }) => {
     const scroll_level =
@@ -20,7 +20,7 @@ const Scrollable = (props) => {
     const currentScroll = Math.ceil(getScrollPrecent(e) / 10);
     if (currentScroll > scrollDepth) {
       setScrollDepth(currentScroll);
-      isLoaded &&
+      mixpanel.get_distinct_id() &&
         mixpanel.people.increment({
           [`Scroll depth at: ${router.nextRouter.pathname}`]: currentScroll,
         });
diff --git a/frontend/src/core/hooks/index.js b/frontend/src/core/hooks/index.js
index 328a74da..5625f907 100644
--- a/frontend/src/core/hooks/index.js
+++ b/frontend/src/core/hooks/index.js
@@ -1,5 +1,4 @@
 export { queryCacheProps as hookCommon } from "./hookCommon";
-export { default as useAnalytics } from "./useAnalytics";
 export { default as useAuthResultHandler } from "./useAuthResultHandler";
 export { default as useChangePassword } from "./useChangePassword";
 export { default as useClientID } from "./useClientID";
diff --git a/frontend/src/core/hooks/useAnalytics.js b/frontend/src/core/hooks/useAnalytics.js
deleted file mode 100644
index fee7e9eb..00000000
--- a/frontend/src/core/hooks/useAnalytics.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import AnalyticsContext from "../providers/AnalyticsProvider/context";
-import { useContext } from "react";
-import { useState, useEffect, useCallback } from "react";
-const useAnalytics = () => {
-  const { mixpanel, isLoaded, MIXPANEL_EVENTS, MIXPANEL_PROPS } =
-    useContext(AnalyticsContext);
-  const [trackProps, setTrackProps] = useState({
-    event: null,
-    props: null,
-    queued: false,
-  });
-
-  const track = useCallback((e, props) => {
-    setTrackProps({ event: e, props: props, queued: true });
-  }, []);
-
-  useEffect(() => {
-    if (isLoaded && trackProps.queued === true) {
-      mixpanel.track(trackProps.event, trackProps.props);
-      setTrackProps({ event: null, props: null, queued: false });
-    }
-  }, [isLoaded, mixpanel, trackProps]);
-
-  const withTracking = (fn, event, props) => {
-    track(event, props);
-    return fn;
-  };
-
-  return {
-    mixpanel,
-    isLoaded,
-    track,
-    MIXPANEL_PROPS,
-    MIXPANEL_EVENTS,
-    withTracking,
-  };
-};
-export default useAnalytics;
diff --git a/frontend/src/core/hooks/useLogin.js b/frontend/src/core/hooks/useLogin.js
index c8dd3bfc..b410cc9a 100644
--- a/frontend/src/core/hooks/useLogin.js
+++ b/frontend/src/core/hooks/useLogin.js
@@ -1,7 +1,6 @@
 import { useMutation } from "react-query";
 import { useToast, useUser, useInviteAccept } from ".";
 import { AuthService } from "../services";
-import { useAnalytics } from ".";
 
 const LOGIN_TYPES = {
   MANUAL: true,
@@ -10,7 +9,6 @@ const LOGIN_TYPES = {
 const useLogin = (loginType) => {
   const { getUser } = useUser();
   const toast = useToast();
-  const analytics = useAnalytics();
   const { inviteAccept } = useInviteAccept();
   const {
     mutate: login,
@@ -34,20 +32,6 @@ const useLogin = (loginType) => {
           inviteAccept(invite_code);
         }
         getUser();
-        if (analytics.isLoaded) {
-          analytics.mixpanel.people.set_once({
-            [`${analytics.MIXPANEL_EVENTS.FIRST_LOGIN_DATE}`]:
-              new Date().toISOString(),
-          });
-          analytics.mixpanel.people.set({
-            [`${analytics.MIXPANEL_EVENTS.LAST_LOGIN_DATE}`]:
-              new Date().toISOString(),
-          });
-          analytics.mixpanel.track(
-            `${analytics.MIXPANEL_EVENTS.USER_LOGS_IN}`,
-            {}
-          );
-        }
       }
     },
     onError: (error) => {
diff --git a/frontend/src/core/hooks/useLogout.js b/frontend/src/core/hooks/useLogout.js
index ff605d80..c8be9a86 100644
--- a/frontend/src/core/hooks/useLogout.js
+++ b/frontend/src/core/hooks/useLogout.js
@@ -1,21 +1,14 @@
 import { useCallback, useContext } from "react";
 import { useMutation, useQueryClient } from "react-query";
-import { useUser, useRouter, useAnalytics } from ".";
+import { useUser, useRouter } from ".";
 import UIContext from "../providers/UIProvider/context";
 import { AuthService } from "../services";
 
 const useLogout = () => {
   const { setLoggingOut } = useContext(UIContext);
   const router = useRouter();
-  const analytics = useAnalytics();
   const { mutate: revoke } = useMutation(AuthService.revoke, {
     onSuccess: () => {
-      if (analytics.isLoaded) {
-        analytics.mixpanel.track(
-          `${analytics.MIXPANEL_EVENTS.USER_LOGS_OUT}`,
-          {}
-        );
-      }
       localStorage.removeItem("MOONSTREAM_ACCESS_TOKEN");
       cache.clear();
       setUser(null);
diff --git a/frontend/src/core/hooks/useSignUp.js b/frontend/src/core/hooks/useSignUp.js
index f6dca897..6ae77f7e 100644
--- a/frontend/src/core/hooks/useSignUp.js
+++ b/frontend/src/core/hooks/useSignUp.js
@@ -1,16 +1,18 @@
 import { useContext } from "react";
 import { useMutation } from "react-query";
 import { AuthService } from "../services";
-import { useUser, useToast, useInviteAccept, useRouter, useAnalytics } from ".";
+import { useUser, useToast, useInviteAccept, useRouter } from ".";
 import UIContext from "../providers/UIProvider/context";
+import mixpanel from "mixpanel-browser";
+import { MIXPANEL_EVENTS } from "../providers/AnalyticsProvider/constants";
 
 const useSignUp = (source) => {
   const ui = useContext(UIContext);
+
   const router = useRouter();
   const { getUser } = useUser();
   const toast = useToast();
   const { inviteAccept } = useInviteAccept();
-  const analytics = useAnalytics();
 
   const {
     mutate: signUp,
@@ -26,11 +28,11 @@ const useSignUp = (source) => {
         inviteAccept(invite_code);
       }
 
-      if (analytics.isLoaded) {
-        analytics.mixpanel.track(
-          `${analytics.MIXPANEL_EVENTS.CONVERT_TO_USER}`,
-          { full_url: router.nextRouter.asPath, code: source }
-        );
+      if (mixpanel.get_distinct_id()) {
+        mixpanel.track(`${MIXPANEL_EVENTS.CONVERT_TO_USER}`, {
+          full_url: router.nextRouter.asPath,
+          code: source,
+        });
       }
       getUser();
       ui.setisOnboardingComplete(false);
diff --git a/frontend/src/core/hooks/useToast.js b/frontend/src/core/hooks/useToast.js
index eeae3e7e..6d78ed9a 100644
--- a/frontend/src/core/hooks/useToast.js
+++ b/frontend/src/core/hooks/useToast.js
@@ -1,21 +1,18 @@
 import { useToast as useChakraToast, Box } from "@chakra-ui/react";
 import React, { useCallback } from "react";
-import { useAnalytics } from ".";
+import mixpanel from "mixpanel-browser";
+import { MIXPANEL_EVENTS } from "../providers/AnalyticsProvider/constants";
 
 const useToast = () => {
   const chakraToast = useChakraToast();
-  const analytics = useAnalytics();
 
   const toast = useCallback(
     (message, type) => {
-      if (analytics.isLoaded && type === "error") {
-        analytics.mixpanel.track(
-          `${analytics.MIXPANEL_EVENTS.TOAST_ERROR_DISPLAYED}`,
-          {
-            status: message?.response?.status,
-            detail: message?.response?.data.detail,
-          }
-        );
+      if (mixpanel.get_distinct_id() && type === "error") {
+        mixpanel.track(`${MIXPANEL_EVENTS.TOAST_ERROR_DISPLAYED}`, {
+          status: message?.response?.status,
+          detail: message?.response?.data.detail,
+        });
       }
       const background = type === "error" ? "unsafe.500" : "suggested.500";
       const userMessage =
@@ -43,7 +40,7 @@ const useToast = () => {
         ),
       });
     },
-    [chakraToast, analytics]
+    [chakraToast]
   );
 
   return toast;
diff --git a/frontend/src/core/providers/AnalyticsProvider/constants.js b/frontend/src/core/providers/AnalyticsProvider/constants.js
index 309532e1..d8dc2a09 100644
--- a/frontend/src/core/providers/AnalyticsProvider/constants.js
+++ b/frontend/src/core/providers/AnalyticsProvider/constants.js
@@ -8,6 +8,12 @@ export const MIXPANEL_PROPS = {
   USER_SPECIALITY: "user speciality",
 };
 
+export const MIXPANEL_GROUPS = {
+  DEVELOPERS: "developers",
+  TRADERS: "traders",
+  FUND: "funds",
+};
+
 export const MIXPANEL_EVENTS = {
   FIRST_LOGIN_DATE: "First login date",
   LAST_LOGIN_DATE: "Last  login date",
@@ -20,7 +26,14 @@ export const MIXPANEL_EVENTS = {
   PAGEVIEW: "Page view",
   PRICING_PLAN_CLICKED: "Pricing Plan clicked",
   BUTTON_CLICKED: "Button clicked",
-  LEFT_PAGE: "Left page",
+  BEACON: "beacon",
+  ONBOARDING_COMPLETED: "Onbording complete",
+  SESSIONS_COUNT: "Sessions Counter",
+  ONBOARDING_STEP: "Onboarding step",
+  ONBOARDING_STATE: "Onboarding state",
+  TIMES_VISITED: "Page visit times",
+  FORM_SUBMITTED: "form submitted",
+  PAGEVIEW_DURATION: "Time spent on page",
 };
 
 export default MIXPANEL_EVENTS;
diff --git a/frontend/src/core/providers/AnalyticsProvider/index.js b/frontend/src/core/providers/AnalyticsProvider/index.js
index eddffb8a..f05b2d85 100644
--- a/frontend/src/core/providers/AnalyticsProvider/index.js
+++ b/frontend/src/core/providers/AnalyticsProvider/index.js
@@ -1,66 +1,161 @@
-import React, { useEffect, useState } from "react";
+import React, { useContext, useEffect, useState } from "react";
 import mixpanel from "mixpanel-browser";
 import AnalyticsContext from "./context";
 import { useClientID, useUser, useRouter } from "../../hooks";
 import { MIXPANEL_EVENTS, MIXPANEL_PROPS } from "./constants";
+import UIContext from "../UIProvider/context";
 
 const AnalyticsProvider = ({ children }) => {
   const clientID = useClientID();
   const analytics = process.env.NEXT_PUBLIC_MIXPANEL_TOKEN;
-  const { user } = useUser();
-  const [isLoaded, setIsLoaded] = useState(false);
+  const { user, isInit } = useUser();
+  const [isMixpanelReady, setIsLoaded] = useState(false);
   const router = useRouter();
+  const ui = useContext(UIContext);
 
+  // ********** OBOARDING STATE **************
+  useEffect(() => {
+    if (ui.onboardingState && isMixpanelReady) {
+      mixpanel.people.set(MIXPANEL_EVENTS.ONBOARDING_STATE, {
+        state: { ...ui.onboardingState },
+      });
+    }
+  }, [ui.onboardingState, isMixpanelReady]);
+
+  useEffect(() => {
+    if (ui.isOnboardingComplete && isMixpanelReady && user) {
+      mixpanel.people.set(MIXPANEL_EVENTS.ONBOARDING_COMPLETED, true);
+    }
+  }, [ui.isOnboardingComplete, isMixpanelReady, user]);
+
+  // ********** ONBOARDING STEP and TIMING **************
+  const [previousOnboardingStep, setPreviousOnboardingStep] = useState(false);
+
+  useEffect(() => {
+    if (isMixpanelReady && router.nextRouter.pathname === "/welcome") {
+      if (!previousOnboardingStep) {
+        mixpanel.time_event(MIXPANEL_EVENTS.ONBOARDING_STEP);
+        setPreviousOnboardingStep(ui.onboardingStep);
+      }
+      if (
+        previousOnboardingStep &&
+        previousOnboardingStep !== ui.onboardingStep
+      ) {
+        mixpanel.track(MIXPANEL_EVENTS.ONBOARDING_STEP, {
+          step: previousOnboardingStep,
+          isBeforeUnload: false,
+        });
+        setPreviousOnboardingStep(false);
+      }
+    } else if (previousOnboardingStep) {
+      mixpanel.track(MIXPANEL_EVENTS.ONBOARDING_STEP, {
+        step: previousOnboardingStep,
+        isBeforeUnload: false,
+      });
+      setPreviousOnboardingStep(false);
+    }
+  }, [
+    previousOnboardingStep,
+    ui.onboardingStep,
+    isMixpanelReady,
+    router.nextRouter.pathname,
+  ]);
+
+  // ********** PING_PONG **************
   useEffect(() => {
     let durationSeconds = 0;
 
     const intervalId =
-      isLoaded &&
+      isMixpanelReady &&
       setInterval(() => {
-        durationSeconds = durationSeconds + 1;
+        durationSeconds = durationSeconds + 30;
         mixpanel.track(
-          MIXPANEL_EVENTS.LEFT_PAGE,
+          MIXPANEL_EVENTS.BEACON,
           {
             duration_seconds: durationSeconds,
             url: router.nextRouter.pathname,
-            query: router.query,
-            pathParams: router.params,
           },
           { transport: "sendBeacon" }
         );
       }, 30000);
 
     return () => clearInterval(intervalId);
-    // eslint-disable-next-line
-  }, [isLoaded]);
+  }, [isMixpanelReady, router.nextRouter.pathname]);
+
+  // ********** TIME SPENT ON PATH**************
+
+  const [previousPathname, setPreviousPathname] = useState(false);
 
   useEffect(() => {
-    isLoaded &&
+    if (isMixpanelReady) {
+      if (!previousPathname) {
+        mixpanel.time_event(MIXPANEL_EVENTS.PAGEVIEW_DURATION);
+        setPreviousPathname(router.nextRouter.pathname);
+      }
+      if (previousPathname && previousPathname !== router.nextRouter.pathname) {
+        mixpanel.track(MIXPANEL_EVENTS.PAGEVIEW_DURATION, {
+          url: previousPathname,
+          isBeforeUnload: false,
+        });
+        setPreviousPathname(false);
+      }
+    }
+  }, [router.nextRouter.pathname, previousPathname, isMixpanelReady]);
+
+  // ********** PAGES VIEW  **************
+  useEffect(() => {
+    if (isMixpanelReady && ui.sessionId && router.nextRouter.pathname) {
       mixpanel.track(MIXPANEL_EVENTS.PAGEVIEW, {
         url: router.nextRouter.pathname,
-        query: router.query,
-        pathParams: router.params,
+        sessionID: ui.sessionId,
       });
-  }, [router.nextRouter.pathname, router.query, router.params, isLoaded]);
 
-  useEffect(() => {
-    try {
-      mixpanel.init(analytics, {
-        api_host: "https://api.mixpanel.com",
-        loaded: () => {
-          setIsLoaded(true);
-          mixpanel.identify(clientID);
-        },
+      mixpanel.people.increment([
+        `${MIXPANEL_EVENTS.TIMES_VISITED} ${router.nextRouter.pathname}`,
+      ]);
+    }
+    const urlForUnmount = router.nextRouter.pathname;
+    const closeListener = () => {
+      mixpanel.track(MIXPANEL_EVENTS.PAGEVIEW_DURATION, {
+        url: urlForUnmount,
+        isBeforeUnload: true,
       });
-    } catch (error) {
-      console.warn("loading mixpanel failed:", error);
+    };
+    window.addEventListener("beforeunload", closeListener);
+    //cleanup function fires on useEffect unmount
+    //https://reactjs.org/docs/hooks-effect.html
+    return () => {
+      window.removeEventListener("beforeunload", closeListener);
+    };
+  }, [router.nextRouter.pathname, isMixpanelReady, ui.sessionId]);
+
+  // ********** SESSION STATE **************
+  useEffect(() => {
+    if (clientID) {
+      try {
+        mixpanel.init(analytics, {
+          api_host: "https://api.mixpanel.com",
+          loaded: () => {
+            setIsLoaded(true);
+            mixpanel.identify(clientID);
+          },
+        });
+      } catch (error) {
+        console.warn("loading mixpanel failed:", error);
+      }
     }
   }, [analytics, clientID]);
 
+  useEffect(() => {
+    isMixpanelReady && mixpanel.register("sessionId", ui.sessionId);
+  }, [ui.sessionId, isMixpanelReady]);
+
+  // ********** USER STATE **************
+
   useEffect(() => {
     if (user) {
       try {
-        if (isLoaded) {
+        if (isMixpanelReady) {
           mixpanel.people.set({
             [`${MIXPANEL_EVENTS.LAST_VISITED}`]: new Date().toISOString(),
           });
@@ -74,11 +169,36 @@ const AnalyticsProvider = ({ children }) => {
         console.error("could not set up people in mixpanel:", err);
       }
     }
-  }, [user, isLoaded, clientID]);
+  }, [user, isMixpanelReady, clientID]);
+
+  useEffect(() => {
+    if (isMixpanelReady && user) {
+      mixpanel.people.set_once({
+        [`${MIXPANEL_EVENTS.FIRST_LOGIN_DATE}`]: new Date().toISOString(),
+      });
+      mixpanel.people.set({
+        [`${MIXPANEL_EVENTS.LAST_LOGIN_DATE}`]: new Date().toISOString(),
+      });
+      mixpanel.track(`${MIXPANEL_EVENTS.USER_LOGS_IN}`, {});
+    }
+  }, [user, isMixpanelReady]);
+
+  useEffect(() => {
+    if (isMixpanelReady && ui.isLoggingOut) {
+      mixpanel.track(`${MIXPANEL_EVENTS.USER_LOGS_OUT}`, {});
+    }
+  }, [ui.isLoggingOut, isMixpanelReady]);
+
+  // ********** USER BOUNCE TIME **************
+  useEffect(() => {
+    if (!user && isInit && isMixpanelReady) {
+      mixpanel.time_event(MIXPANEL_EVENTS.CONVERT_TO_USER);
+    }
+  }, [user, isInit, isMixpanelReady]);
 
   return (
     <AnalyticsContext.Provider
-      value={{ mixpanel, isLoaded, MIXPANEL_EVENTS, MIXPANEL_PROPS }}
+      value={{ mixpanel, isMixpanelReady, MIXPANEL_EVENTS, MIXPANEL_PROPS }}
     >
       {children}
     </AnalyticsContext.Provider>
diff --git a/frontend/src/core/providers/UIProvider/index.js b/frontend/src/core/providers/UIProvider/index.js
index 9c92d494..d20223a8 100644
--- a/frontend/src/core/providers/UIProvider/index.js
+++ b/frontend/src/core/providers/UIProvider/index.js
@@ -181,10 +181,19 @@ const UIProvider = ({ children }) => {
   );
 
   const [onboardingStep, setOnboardingStep] = useState(() => {
-    const step = onboardingSteps.findIndex(
-      (event) => onboardingState[event.step] === 0
+    //First find onboarding step that was viewed once (resume where left)
+    // If none - find step that was never viewed
+    // if none - set onboarding to zero
+    let step = onboardingSteps.findIndex(
+      (event) => onboardingState[event.step] === 1
     );
-    if (step === -1 && isOnboardingComplete) return onboardingSteps.length - 1;
+    step =
+      step === -1
+        ? onboardingSteps.findIndex(
+            (event) => onboardingState[event.step] === 0
+          )
+        : step;
+    if (step === -1 && isOnboardingComplete) return 0;
     else if (step === -1 && !isOnboardingComplete) return 0;
     else return step;
   });
@@ -263,6 +272,8 @@ const UIProvider = ({ children }) => {
         setisOnboardingComplete,
         onboardingSteps,
         setOnboardingState,
+        onboardingState,
+        isLoggingOut,
       }}
     >
       {children}

From 212ce8bb3c66c229120c815e4a73f9e50a8c4cdf Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Tue, 7 Sep 2021 16:02:40 +0300
Subject: [PATCH 02/14] Fix.

---
 backend/moonstream/data.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/backend/moonstream/data.py b/backend/moonstream/data.py
index 901ef78f..df4b6537 100644
--- a/backend/moonstream/data.py
+++ b/backend/moonstream/data.py
@@ -25,6 +25,7 @@ class SubscriptionTypesListResponse(BaseModel):
 
 class SubscriptionResourceData(BaseModel):
     id: str
+    address: str
     color: Optional[str]
     label: Optional[str]
     user_id: str

From be233e644a4a0536d7353cc25230ee773299df9e Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Tue, 7 Sep 2021 18:45:48 +0300
Subject: [PATCH 03/14] Add fixes.

---
 frontend/pages/subscriptions.js            |  18 ++-
 frontend/src/components/NewSubscription.js | 137 ++++++++++++---------
 2 files changed, 90 insertions(+), 65 deletions(-)

diff --git a/frontend/pages/subscriptions.js b/frontend/pages/subscriptions.js
index fe0724cd..b8185eea 100644
--- a/frontend/pages/subscriptions.js
+++ b/frontend/pages/subscriptions.js
@@ -12,10 +12,13 @@ import {
   Button,
   Modal,
   useDisclosure,
+  ModalHeader,
+  ModalCloseButton,
+  ModalBody,
   ModalOverlay,
   ModalContent,
 } from "@chakra-ui/react";
-import NewSubscription from "../src/components/NewModalSubscripton";
+import NewSubscription from "../src/components/NewSubscription";
 import { AiOutlinePlusCircle } from "react-icons/ai";
 
 const Subscriptions = () => {
@@ -52,10 +55,15 @@ const Subscriptions = () => {
         <ModalOverlay />
 
         <ModalContent>
-          <NewSubscription
-            isFreeOption={isAddingFreeSubscription}
-            onClose={onClose}
-          />
+          <ModalHeader>Subscribe to a new address</ModalHeader>
+          <ModalCloseButton />
+          <ModalBody>
+            <NewSubscription
+              isFreeOption={isAddingFreeSubscription}
+              onClose={onClose}
+              isModal={true}
+            />
+          </ModalBody>
         </ModalContent>
       </Modal>
       {subscriptionsCache.isLoading ? (
diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js
index 284e3c3b..fa5efd72 100644
--- a/frontend/src/components/NewSubscription.js
+++ b/frontend/src/components/NewSubscription.js
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useCallback } from "react";
+import React, { useState, useEffect, useCallback, Fragment } from "react";
 import { useSubscriptions } from "../core/hooks";
 import {
   Input,
@@ -19,6 +19,7 @@ import RadioCard from "./RadioCard";
 // import { useForm } from "react-hook-form";
 import { CirclePicker } from "react-color";
 import { BiRefresh } from "react-icons/bi";
+import { GithubPicker } from "react-color";
 import { makeColor } from "../core/utils/makeColor";
 import { useForm } from "react-hook-form";
 const _NewSubscription = ({
@@ -27,6 +28,7 @@ const _NewSubscription = ({
   setIsLoading,
   initialAddress,
   initialType,
+  isModal,
 }) => {
   const [color, setColor] = useState(makeColor());
   const { handleSubmit, errors, register } = useForm({});
@@ -36,8 +38,13 @@ const _NewSubscription = ({
     initialType ?? "ethereum_blockchain"
   );
 
+  const mapper = {
+    "tag:nfts": "NFTs",
+    "input:address": "Address",
+  };
+
   const [subscriptionAdressFormatRadio, setsubscriptionAdressFormatRadio] =
-    useState("");
+    useState("input:address");
 
   let { getRootProps, getRadioProps } = useRadioGroup({
     name: "type",
@@ -50,6 +57,7 @@ const _NewSubscription = ({
     getRadioProps: getRadioPropsSubscription,
   } = useRadioGroup({
     name: "subscription",
+    defaultValue: subscriptionAdressFormatRadio,
     onChange: setsubscriptionAdressFormatRadio,
   });
 
@@ -78,11 +86,13 @@ const _NewSubscription = ({
 
   const createSubscriptionWrapper = useCallback(
     (props) => {
+      props.label = "Address";
       if (
         subscriptionAdressFormatRadio.startsWith("tags") &&
         radioState != "ethereum_whalewatch"
       ) {
-        props.address = subscriptionAdressFormatRadio.split(":")[1];
+        props.address = subscriptionAdressFormatRadio;
+        props.label = "tags: NFTs";
       }
 
       createSubscription.mutate({
@@ -104,8 +114,6 @@ const _NewSubscription = ({
     }
   }
 
-  console.log(radioState);
-
   const handleChangeColorComplete = (color) => {
     setColor(color.hex);
   };
@@ -114,26 +122,7 @@ const _NewSubscription = ({
 
   return (
     <form onSubmit={handleSubmit(createSubscriptionWrapper)}>
-      <FormControl isInvalid={errors?.label}>
-        <Input
-          my={2}
-          type="text"
-          autoComplete="off"
-          placeholder="Name of subscription (you can change it later)"
-          name="label"
-          ref={register({ required: "label is required!" })}
-        ></Input>
-        <FormErrorMessage color="unsafe.400" pl="1">
-          {errors?.label && errors?.label.message}
-        </FormErrorMessage>
-      </FormControl>
       <Stack my={4} direction="column">
-        {/* <Text fontWeight="600">
-          {isFreeOption
-            ? `Right now you can subscribe only to ethereum blockchain`
-            : `On which source?`}
-        </Text> */}
-
         <FormControl isInvalid={errors?.subscription_type}>
           <HStack {...group} alignItems="stretch">
             {typesCache.data.map((type) => {
@@ -144,7 +133,6 @@ const _NewSubscription = ({
                   !type.active ||
                   (isFreeOption && type.id !== "ethereum_blockchain"),
               });
-              console.log(type);
 
               return (
                 <RadioCard key={`subscription_type_${type.id}`} {...radio}>
@@ -158,19 +146,19 @@ const _NewSubscription = ({
               <HStack {...subscriptionAddressTypeGroup} alignItems="stretch">
                 {search(radioState, typesCache.data).choices.map(
                   (addition_selects) => {
+                    console.log(typeof addition_selects);
                     const radio = getRadioPropsSubscription({
                       value: addition_selects,
+                      isDisabled: addition_selects.startsWith("tag"),
                     });
-                    console.log(radio);
                     return (
                       <RadioCard
                         key={`subscription_tags_${addition_selects}`}
                         {...radio}
                       >
-                        {addition_selects.split(":")[1]}
+                        {mapper[addition_selects]}
                       </RadioCard>
                     );
-                    // }
                   }
                 )}
               </HStack>
@@ -207,40 +195,69 @@ const _NewSubscription = ({
         </FormControl>
       </Stack>
       <FormControl isInvalid={errors?.color}>
-        <Flex direction="row" pb={2} flexWrap="wrap">
-          <Stack pt={2} direction="row" h="min-content">
-            <Text fontWeight="600" alignSelf="center">
-              Label color
-            </Text>{" "}
-            <IconButton
-              size="md"
-              // colorScheme="primary"
-              color={"white.100"}
-              _hover={{ bgColor: { color } }}
-              bgColor={color}
-              variant="outline"
-              onClick={() => setColor(makeColor())}
-              icon={<BiRefresh />}
-            />
-            <Input
-              type="input"
-              placeholder="color"
-              name="color"
-              ref={register({ required: "color is required!" })}
-              value={color}
-              onChange={() => null}
-              w="200px"
-            ></Input>
-          </Stack>
-          <Flex p={2}>
-            <CirclePicker
-              onChangeComplete={handleChangeColorComplete}
-              circleSpacing={1}
-              circleSize={24}
-            />
+        {!isModal ? (
+          <Flex direction="row" pb={2} flexWrap="wrap">
+            <Stack pt={2} direction="row" h="min-content">
+              <Text fontWeight="600" alignSelf="center">
+                Label color
+              </Text>{" "}
+              <IconButton
+                size="md"
+                // colorScheme="primary"
+                color={"white.100"}
+                _hover={{ bgColor: { color } }}
+                bgColor={color}
+                variant="outline"
+                onClick={() => setColor(makeColor())}
+                icon={<BiRefresh />}
+              />
+              <Input
+                type="input"
+                placeholder="color"
+                name="color"
+                ref={register({ required: "color is required!" })}
+                value={color}
+                onChange={() => null}
+                w="200px"
+              ></Input>
+            </Stack>
+            <Flex p={2}>
+              <CirclePicker
+                onChangeComplete={handleChangeColorComplete}
+                circleSpacing={1}
+                circleSize={24}
+              />
+            </Flex>
           </Flex>
-        </Flex>
+        ) : (
+          <>
+            <Stack direction="row" pb={2}>
+              <Text fontWeight="600" alignSelf="center">
+                Label color
+              </Text>{" "}
+              <IconButton
+                size="md"
+                color={"white.100"}
+                _hover={{ bgColor: { color } }}
+                bgColor={color}
+                variant="outline"
+                onClick={() => setColor(makeColor())}
+                icon={<BiRefresh />}
+              />
+              <Input
+                type="input"
+                placeholder="color"
+                name="color"
+                ref={register({ required: "color is required!" })}
+                value={color}
+                onChange={() => null}
+                w="200px"
+              ></Input>
+            </Stack>
 
+            <GithubPicker onChangeComplete={handleChangeColorComplete} />
+          </>
+        )}
         <FormErrorMessage color="unsafe.400" pl="1">
           {errors?.color && errors?.color.message}
         </FormErrorMessage>

From 5a7b261b006373120a7f297994c5441401f0376f Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Tue, 7 Sep 2021 18:56:12 +0300
Subject: [PATCH 04/14] Update subscriptions cli.

---
 backend/moonstream/admin/subscription_types.py | 14 +++++---------
 1 file changed, 5 insertions(+), 9 deletions(-)

diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py
index 0c668910..6f97c21e 100644
--- a/backend/moonstream/admin/subscription_types.py
+++ b/backend/moonstream/admin/subscription_types.py
@@ -20,7 +20,7 @@ CANONICAL_SUBSCRIPTION_TYPES = {
     "ethereum_blockchain": SubscriptionTypeResourceData(
         id="ethereum_blockchain",
         name="Ethereum transactions",
-        choices=["input:Address", "tag:nfts"],
+        choices=["input:address", "tag:nfts"],
         description="Transactions that have been mined into the Ethereum blockchain",
         icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-purple.png",
         stripe_product_id=None,
@@ -42,7 +42,7 @@ CANONICAL_SUBSCRIPTION_TYPES = {
         id="ethereum_txpool",
         name="Ethereum transaction pool",
         description="Transactions that have been submitted into the Ethereum transaction pool but not necessarily mined yet",
-        choices=["input:Address", "tag:nfts"],
+        choices=["input:address", "tag:nfts"],
         icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-rainbow.png",
         stripe_product_id=None,
         stripe_price_id=None,
@@ -274,19 +274,15 @@ def update_subscription_type(
     # TODO(zomglings): This was written with an outdated bugout-python client.
     # New client has an update_resource method which is what we should be using
     # here.
-    new_resource = bc.create_resource(
-        token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
-        application_id=MOONSTREAM_APPLICATION_ID,
-        resource_data=updated_resource_data,
-        timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
-    )
 
     try:
-        bc.delete_resource(
+        new_resource = bc.update_resource(
             token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
             resource_id=brood_resource_id,
+            resource_data={"update": updated_resource_data},
             timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
         )
+
     except Exception as e:
         raise ConflictingSubscriptionTypesError(
             f"Unable to delete old subscription type with ID: {id}. Error:\n{repr(e)}"

From 6105a4402f98b2530460401b3a2a60255be87370 Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Tue, 7 Sep 2021 19:42:31 +0300
Subject: [PATCH 05/14] Update ensure canonical.

---
 backend/moonstream/admin/subscription_types.py | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py
index 6f97c21e..51e9976a 100644
--- a/backend/moonstream/admin/subscription_types.py
+++ b/backend/moonstream/admin/subscription_types.py
@@ -76,7 +76,7 @@ def create_subscription_type(
     id: str,
     name: str,
     description: str,
-    choices: List[str],
+    choices: Optional[List[str]],
     icon_url: str,
     stripe_product_id: Optional[str] = None,
     stripe_price_id: Optional[str] = None,
@@ -115,6 +115,7 @@ def create_subscription_type(
         "id": id,
         "name": name,
         "description": description,
+        "choices": choices,
         "icon_url": icon_url,
         "stripe_product_id": stripe_product_id,
         "stripe_price_id": stripe_price_id,
@@ -377,6 +378,19 @@ def ensure_canonical_subscription_types() -> BugoutResources:
                 canonical_subscription_type.active,
             )
             existing_canonical_subscription_types[id] = resource
+        else:
+            canonical_subscription_type = CANONICAL_SUBSCRIPTION_TYPES[id]
+            resource = update_subscription_type(
+                id,
+                canonical_subscription_type.name,
+                canonical_subscription_type.description,
+                canonical_subscription_type.choices,
+                canonical_subscription_type.icon_url,
+                canonical_subscription_type.stripe_product_id,
+                canonical_subscription_type.stripe_price_id,
+                canonical_subscription_type.active,
+            )
+            existing_canonical_subscription_types[id] = resource
 
     return BugoutResources(
         resources=list(existing_canonical_subscription_types.values())

From 42f75b263f5bcba323a238e205b492ed7b410358 Mon Sep 17 00:00:00 2001
From: Tim Pechersky <tim.pechersky@gmail.com>
Date: Tue, 7 Sep 2021 22:53:48 +0200
Subject: [PATCH 06/14] layout improvements

---
 frontend/pages/welcome.js                     |  21 +-
 frontend/src/Theme/Tooltip/index.js           |  19 ++
 .../src/components/NewModalSubscripton.js     | 182 ------------------
 frontend/src/components/NewSubscription.js    | 174 +++++++++--------
 frontend/src/components/RadioCard.js          |  74 ++++---
 .../stream-cards/EthereumWhalewatch.js        |   2 +-
 6 files changed, 171 insertions(+), 301 deletions(-)
 delete mode 100644 frontend/src/components/NewModalSubscripton.js

diff --git a/frontend/pages/welcome.js b/frontend/pages/welcome.js
index f5b8c643..d861a770 100644
--- a/frontend/pages/welcome.js
+++ b/frontend/pages/welcome.js
@@ -23,6 +23,7 @@ import {
   AccordionButton,
   AccordionPanel,
   AccordionIcon,
+  Divider,
 } from "@chakra-ui/react";
 import StepProgress from "../src/components/StepProgress";
 import { ArrowLeftIcon, ArrowRightIcon } from "@chakra-ui/icons";
@@ -95,7 +96,7 @@ const Welcome = () => {
           <Fade in>
             <Stack spacing={4}>
               <Stack
-                px={12}
+                px={[0, 12, null]}
                 // mt={24}
                 bgColor="gray.50"
                 borderRadius="xl"
@@ -119,7 +120,7 @@ const Welcome = () => {
                 </Text>
               </Stack>
               <Stack
-                px={12}
+                px={[0, 12, null]}
                 // mt={24}
                 bgColor="gray.50"
                 borderRadius="xl"
@@ -176,7 +177,7 @@ const Welcome = () => {
                 </Accordion>
               </Stack>
               <Stack
-                px={12}
+                px={[0, 12, null]}
                 // mt={24}
                 bgColor="gray.50"
                 borderRadius="xl"
@@ -298,7 +299,7 @@ const Welcome = () => {
           <Fade in>
             <Stack px="7%">
               <Stack
-                px={12}
+                px={[0, 12, null]}
                 // mt={24}
                 bgColor="gray.50"
                 borderRadius="xl"
@@ -345,14 +346,18 @@ const Welcome = () => {
                 )}
               <SubscriptionsList />
               {showSubscriptionForm && (
-                <>
-                  <Heading pt={12}>{`Let's add new subscription!`}</Heading>
+                <Flex direction="column" pt={6}>
+                  <Divider bgColor="gray.500" borderWidth="2px" />
+                  <Heading
+                    size="md"
+                    pt={2}
+                  >{`Let's add new subscription!`}</Heading>
 
                   <NewSubscription
                     isFreeOption={false}
                     onClose={SubscriptonCreatedCallback}
                   />
-                </>
+                </Flex>
               )}
               {!showSubscriptionForm && (
                 <Button
@@ -370,7 +375,7 @@ const Welcome = () => {
           <Fade in>
             <Stack>
               <Stack
-                px={12}
+                px={[0, 12, null]}
                 // mt={24}
                 bgColor="gray.50"
                 borderRadius="xl"
diff --git a/frontend/src/Theme/Tooltip/index.js b/frontend/src/Theme/Tooltip/index.js
index 94c8b27a..1095da35 100644
--- a/frontend/src/Theme/Tooltip/index.js
+++ b/frontend/src/Theme/Tooltip/index.js
@@ -18,6 +18,24 @@ const baseStyle = (props) => {
   };
 };
 
+const variantSuggestion = (props) => {
+  const bg = mode("primary.700", "primary.300")(props);
+  return {
+    "--tooltip-bg": `colors.${bg}`,
+    px: "8px",
+    py: "2px",
+    bg: "var(--tooltip-bg)",
+    "--popper-arrow-bg": "var(--tooltip-bg)",
+    color: mode("whiteAlpha.900", "gray.900")(props),
+    borderRadius: "md",
+    fontWeight: "medium",
+    fontSize: "sm",
+    boxShadow: "md",
+    maxW: "320px",
+    zIndex: "tooltip",
+  };
+};
+
 const variantOnboarding = (props) => {
   const bg = mode("secondary.700", "secondary.300")(props);
   return {
@@ -38,6 +56,7 @@ const variantOnboarding = (props) => {
 
 const variants = {
   onboarding: variantOnboarding,
+  suggestion: variantSuggestion,
 };
 
 export default {
diff --git a/frontend/src/components/NewModalSubscripton.js b/frontend/src/components/NewModalSubscripton.js
deleted file mode 100644
index 9fb40a2b..00000000
--- a/frontend/src/components/NewModalSubscripton.js
+++ /dev/null
@@ -1,182 +0,0 @@
-import React, { useState, useEffect } from "react";
-import { useSubscriptions } from "../core/hooks";
-import {
-  Input,
-  Stack,
-  Text,
-  HStack,
-  useRadioGroup,
-  FormControl,
-  FormErrorMessage,
-  ModalBody,
-  ModalCloseButton,
-  ModalHeader,
-  Button,
-  ModalFooter,
-  Spinner,
-  IconButton,
-} from "@chakra-ui/react";
-import RadioCard from "./RadioCard";
-import { useForm } from "react-hook-form";
-import { GithubPicker } from "react-color";
-import { BiRefresh } from "react-icons/bi";
-import { makeColor } from "../core/utils/makeColor";
-const NewSubscription = ({
-  isFreeOption,
-  onClose,
-  initialAddress,
-  initialType,
-}) => {
-  const [color, setColor] = useState(makeColor());
-  const { typesCache, createSubscription } = useSubscriptions();
-  const { handleSubmit, errors, register } = useForm({});
-  const [radioState, setRadioState] = useState(
-    initialType ?? "ethereum_blockchain"
-  );
-  let { getRootProps, getRadioProps } = useRadioGroup({
-    name: "type",
-    defaultValue: radioState,
-    onChange: setRadioState,
-  });
-
-  const group = getRootProps();
-
-  useEffect(() => {
-    if (createSubscription.isSuccess) {
-      onClose();
-    }
-  }, [createSubscription.isSuccess, onClose]);
-
-  if (typesCache.isLoading) return <Spinner />;
-
-  const createSubscriptionWrap = (props) => {
-    createSubscription.mutate({
-      ...props,
-      color: color,
-      type: isFreeOption ? "free" : radioState,
-    });
-  };
-
-  const handleChangeColorComplete = (color) => {
-    setColor(color.hex);
-  };
-
-  return (
-    <form onSubmit={handleSubmit(createSubscriptionWrap)}>
-      <ModalHeader>Subscribe to a new address</ModalHeader>
-      <ModalCloseButton />
-      <ModalBody>
-        <FormControl isInvalid={errors.label}>
-          <Input
-            my={2}
-            type="text"
-            autoComplete="off"
-            placeholder="Enter label"
-            name="label"
-            ref={register({ required: "label is required!" })}
-          ></Input>
-          <FormErrorMessage color="unsafe.400" pl="1">
-            {errors.label && errors.label.message}
-          </FormErrorMessage>
-        </FormControl>
-        <FormControl isInvalid={errors.address}>
-          <Input
-            type="text"
-            autoComplete="off"
-            my={2}
-            placeholder="Enter address"
-            defaultValue={initialAddress ?? undefined}
-            isReadOnly={!!initialAddress}
-            name="address"
-            ref={register({ required: "address is required!" })}
-          ></Input>
-          <FormErrorMessage color="unsafe.400" pl="1">
-            {errors.address && errors.address.message}
-          </FormErrorMessage>
-        </FormControl>
-        <Stack my={16} direction="column">
-          <Text fontWeight="600">
-            {isFreeOption
-              ? `Free subscription is only availible Ethereum blockchain source`
-              : `On which source?`}
-          </Text>
-
-          <FormControl isInvalid={errors.subscription_type}>
-            <HStack {...group} alignItems="stretch">
-              {typesCache.data.map((type) => {
-                const radio = getRadioProps({
-                  value: type.id,
-                  isDisabled:
-                    (initialAddress && initialType) ||
-                    !type.active ||
-                    (isFreeOption && type.id !== "ethereum_blockchain"),
-                });
-                return (
-                  <RadioCard key={`subscription_type_${type.id}`} {...radio}>
-                    {type.name}
-                  </RadioCard>
-                );
-              })}
-            </HStack>
-            <Input
-              type="hidden"
-              placeholder="subscription_type"
-              name="subscription_type"
-              ref={register({ required: "select type" })}
-              value={radioState}
-              onChange={() => null}
-            ></Input>
-            <FormErrorMessage color="unsafe.400" pl="1">
-              {errors.subscription_type && errors.subscription_type.message}
-            </FormErrorMessage>
-          </FormControl>
-        </Stack>
-        <FormControl isInvalid={errors.color}>
-          <Stack direction="row" pb={2}>
-            <Text fontWeight="600" alignSelf="center">
-              Label color
-            </Text>{" "}
-            <IconButton
-              size="md"
-              color={"white.100"}
-              _hover={{ bgColor: { color } }}
-              bgColor={color}
-              variant="outline"
-              onClick={() => setColor(makeColor())}
-              icon={<BiRefresh />}
-            />
-            <Input
-              type="input"
-              placeholder="color"
-              name="color"
-              ref={register({ required: "color is required!" })}
-              value={color}
-              onChange={() => null}
-              w="200px"
-            ></Input>
-          </Stack>
-
-          <GithubPicker onChangeComplete={handleChangeColorComplete} />
-
-          <FormErrorMessage color="unsafe.400" pl="1">
-            {errors.color && errors.color.message}
-          </FormErrorMessage>
-        </FormControl>
-      </ModalBody>
-      <ModalFooter>
-        <Button
-          type="submit"
-          colorScheme="suggested"
-          isLoading={createSubscription.isLoading}
-        >
-          Confirm
-        </Button>
-        <Button colorScheme="gray" onClick={onClose}>
-          Cancel
-        </Button>
-      </ModalFooter>
-    </form>
-  );
-};
-
-export default NewSubscription;
diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js
index fa5efd72..259f8319 100644
--- a/frontend/src/components/NewSubscription.js
+++ b/frontend/src/components/NewSubscription.js
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useCallback, Fragment } from "react";
+import React, { useState, useEffect, useCallback } from "react";
 import { useSubscriptions } from "../core/hooks";
 import {
   Input,
@@ -12,7 +12,6 @@ import {
   Spinner,
   IconButton,
   ButtonGroup,
-  Spacer,
   Flex,
 } from "@chakra-ui/react";
 import RadioCard from "./RadioCard";
@@ -33,7 +32,7 @@ const _NewSubscription = ({
   const [color, setColor] = useState(makeColor());
   const { handleSubmit, errors, register } = useForm({});
   const { typesCache, createSubscription } = useSubscriptions();
-  // const { handleSubmit, errors, register } = useForm({});
+
   const [radioState, setRadioState] = useState(
     initialType ?? "ethereum_blockchain"
   );
@@ -46,31 +45,19 @@ const _NewSubscription = ({
   const [subscriptionAdressFormatRadio, setsubscriptionAdressFormatRadio] =
     useState("input:address");
 
-  let { getRootProps, getRadioProps } = useRadioGroup({
+  let { getRadioProps } = useRadioGroup({
     name: "type",
     defaultValue: radioState,
     onChange: setRadioState,
   });
 
-  let {
-    getRootProps: getRootPropsSubscription,
-    getRadioProps: getRadioPropsSubscription,
-  } = useRadioGroup({
+  let { getRadioProps: getRadioPropsSubscription } = useRadioGroup({
     name: "subscription",
     defaultValue: subscriptionAdressFormatRadio,
     onChange: setsubscriptionAdressFormatRadio,
   });
 
-  console.log(
-    useRadioGroup({
-      name: "subscription1",
-      onChange: setsubscriptionAdressFormatRadio,
-    })
-  );
 
-  const group = getRootProps();
-
-  const subscriptionAddressTypeGroup = getRootPropsSubscription();
 
   useEffect(() => {
     if (setIsLoading) {
@@ -101,7 +88,13 @@ const _NewSubscription = ({
         type: isFreeOption ? "ethereum_blockchain" : radioState,
       });
     },
-    [createSubscription, isFreeOption, color, radioState]
+    [
+      createSubscription,
+      isFreeOption,
+      color,
+      radioState,
+      subscriptionAdressFormatRadio,
+    ]
   );
 
   if (typesCache.isLoading) return <Spinner />;
@@ -123,8 +116,16 @@ const _NewSubscription = ({
   return (
     <form onSubmit={handleSubmit(createSubscriptionWrapper)}>
       <Stack my={4} direction="column">
-        <FormControl isInvalid={errors?.subscription_type}>
-          <HStack {...group} alignItems="stretch">
+        <Stack spacing={1} w="100%">
+          <Text fontWeight="600">Source:</Text>
+          {/* position must be relative otherwise radio boxes add strange spacing on selection */}
+          <Stack
+            spacing={1}
+            w="100%"
+            direction="row"
+            flexWrap="wrap"
+            position="relative"
+          >
             {typesCache.data.map((type) => {
               const radio = getRadioProps({
                 value: type.id,
@@ -135,72 +136,88 @@ const _NewSubscription = ({
               });
 
               return (
-                <RadioCard key={`subscription_type_${type.id}`} {...radio}>
-                  {type.name}
+                <RadioCard
+                  px="8px"
+                  py="4px"
+                  mt="2px"
+                  w="190px"
+                  {...radio}
+                  key={`subscription_type_${type.id}`}
+                  label={type.description}
+                  iconURL={type.icon_url}
+                >
+                  {type.name.slice(9, type.name.length)}
                 </RadioCard>
               );
             })}
-          </HStack>
-          <Stack my={4} direction="column">
-            <FormControl isInvalid={errors?.subscription_type}>
-              <HStack {...subscriptionAddressTypeGroup} alignItems="stretch">
-                {search(radioState, typesCache.data).choices.map(
-                  (addition_selects) => {
-                    console.log(typeof addition_selects);
-                    const radio = getRadioPropsSubscription({
-                      value: addition_selects,
-                      isDisabled: addition_selects.startsWith("tag"),
-                    });
-                    return (
-                      <RadioCard
-                        key={`subscription_tags_${addition_selects}`}
-                        {...radio}
-                      >
-                        {mapper[addition_selects]}
-                      </RadioCard>
-                    );
-                  }
-                )}
-              </HStack>
-            </FormControl>
           </Stack>
+        </Stack>
 
+        <Flex direction="row" w="100%" flexWrap="wrap" pt={4}>
+          {/* position must be relative otherwise radio boxes add strange spacing on selection */}
+          <HStack flexGrow={0} flexBasis="140px" position="relative">
+            {search(radioState, typesCache.data).choices.length > 0 && (
+              <Text fontWeight="600">Type:</Text>
+            )}
+            {search(radioState, typesCache.data).choices.map(
+              (addition_selects) => {
+                const radio = getRadioPropsSubscription({
+                  value: addition_selects,
+                  isDisabled: addition_selects.startsWith("tag"),
+                });
+                return (
+                  <RadioCard
+                    px="4px"
+                    py="2px"
+                    key={`subscription_tags_${addition_selects}`}
+                    {...radio}
+                  >
+                    {mapper[addition_selects]}
+                  </RadioCard>
+                );
+              }
+            )}
+          </HStack>
           {subscriptionAdressFormatRadio.startsWith("input") &&
             radioState != "ethereum_whalewatch" && (
-              <FormControl isInvalid={errors?.address}>
-                <Input
-                  type="text"
-                  autoComplete="off"
-                  my={2}
-                  placeholder="Address to subscribe to"
-                  name="address"
-                  ref={register({ required: "address is required!" })}
-                ></Input>
-                <FormErrorMessage color="unsafe.400" pl="1">
-                  {errors?.address && errors?.address.message}
-                </FormErrorMessage>
-              </FormControl>
+              <Flex flexBasis="240px" flexGrow={1}>
+                <FormControl isInvalid={errors?.address}>
+                  <Input
+                    type="text"
+                    autoComplete="off"
+                    my={2}
+                    placeholder="Address to subscribe to"
+                    name="address"
+                    ref={register({ required: "address is required!" })}
+                  ></Input>
+                  <FormErrorMessage color="unsafe.400" pl="1">
+                    {errors?.address && errors?.address.message}
+                  </FormErrorMessage>
+                </FormControl>
+              </Flex>
             )}
-          <Input
-            type="hidden"
-            placeholder="subscription_type"
-            name="subscription_type"
-            ref={register({ required: "select type" })}
-            value={radioState}
-            onChange={() => null}
-          ></Input>
-          <FormErrorMessage color="unsafe.400" pl="1">
-            {errors?.subscription_type && errors?.subscription_type.message}
-          </FormErrorMessage>
-        </FormControl>
+        </Flex>
+        <Input
+          type="hidden"
+          placeholder="subscription_type"
+          name="subscription_type"
+          ref={register({ required: "select type" })}
+          value={radioState}
+          onChange={() => null}
+        ></Input>
       </Stack>
       <FormControl isInvalid={errors?.color}>
         {!isModal ? (
-          <Flex direction="row" pb={2} flexWrap="wrap">
-            <Stack pt={2} direction="row" h="min-content">
-              <Text fontWeight="600" alignSelf="center">
-                Label color
-              </Text>{" "}
+          <Flex direction="row" pb={2} flexWrap="wrap" alignItems="baseline">
+            <Text fontWeight="600" alignSelf="center">
+              Label color
+            </Text>{" "}
+            <Stack
+              // pt={2}
+              direction={["row", "row", null]}
+              h="min-content"
+              alignSelf="center"
+            >
               <IconButton
                 size="md"
                 // colorScheme="primary"
@@ -221,8 +238,9 @@ const _NewSubscription = ({
                 w="200px"
               ></Input>
             </Stack>
-            <Flex p={2}>
+            <Flex p={2} flexBasis="120px" flexGrow={1} alignSelf="center">
               <CirclePicker
+                width="100%"
                 onChangeComplete={handleChangeColorComplete}
                 circleSpacing={1}
                 circleSize={24}
@@ -263,7 +281,7 @@ const _NewSubscription = ({
         </FormErrorMessage>
       </FormControl>
 
-      <ButtonGroup direction="row" justifyContent="space-evenly">
+      <ButtonGroup direction="row" justifyContent="flex-end" w="100%">
         <Button
           type="submit"
           colorScheme="suggested"
@@ -271,7 +289,7 @@ const _NewSubscription = ({
         >
           Confirm
         </Button>
-        <Spacer />
+
         <Button colorScheme="gray" onClick={onClose}>
           Cancel
         </Button>
diff --git a/frontend/src/components/RadioCard.js b/frontend/src/components/RadioCard.js
index c6cc46cb..17882fdd 100644
--- a/frontend/src/components/RadioCard.js
+++ b/frontend/src/components/RadioCard.js
@@ -1,5 +1,5 @@
 import React from "react";
-import { useRadio, Box, Flex } from "@chakra-ui/react";
+import { useRadio, Box, Flex, Tooltip, Image } from "@chakra-ui/react";
 
 const RadioCard = (props) => {
   const { getInputProps, getCheckboxProps } = useRadio(props);
@@ -8,38 +8,48 @@ const RadioCard = (props) => {
   const checkbox = getCheckboxProps();
 
   return (
-    <Flex as="label" h="fill-availible">
-      <input {...input} />
-      <Box
-        justifyContent="left"
-        alignContent="center"
-        {...checkbox}
-        cursor="pointer"
-        borderWidth="1px"
-        borderRadius="md"
-        boxShadow="md"
-        _disabled={{
-          bg: "gray.300",
-          color: "gray.900",
-          borderColor: "gray.300",
-        }}
-        _checked={{
-          bg: "secondary.900",
-          color: "white",
-          borderColor: "secondary.900",
-        }}
-        _focus={{
-          boxShadow: "outline",
-        }}
-        px={5}
-        py={3}
-        fontWeight="600"
-      >
-        {props.children}
-      </Box>
-    </Flex>
+    <Tooltip
+      hidden={props.label ? false : true}
+      label={props.label}
+      variant="suggestion"
+      openDelay={500}
+    >
+      <Flex as="label" h="fill-availible">
+        <input {...input} />
+        <Box
+          alignContent="center"
+          {...checkbox}
+          cursor="pointer"
+          borderWidth="1px"
+          borderRadius="lg"
+          boxShadow="md"
+          _disabled={{
+            bg: "gray.300",
+            color: "gray.900",
+            borderColor: "gray.300",
+          }}
+          _checked={{
+            // bg: "secondary.900",
+
+            color: "secondary.900",
+            borderColor: "secondary.900",
+            borderWidth: "4px",
+          }}
+          justifyContent="center"
+          px={props.px}
+          mt={props.mt}
+          py={props.py}
+          w={props.w}
+          fontWeight="600"
+        >
+          {props.iconURL && (
+            <Image display="inline-block" w="16px" src={props.iconURL} />
+          )}{" "}
+          {props.children}
+        </Box>
+      </Flex>
+    </Tooltip>
   );
 };
 
-// const RadioCard = chakra(RadioCard_);
 export default RadioCard;
diff --git a/frontend/src/components/stream-cards/EthereumWhalewatch.js b/frontend/src/components/stream-cards/EthereumWhalewatch.js
index c412a8d0..b1f5cb31 100644
--- a/frontend/src/components/stream-cards/EthereumWhalewatch.js
+++ b/frontend/src/components/stream-cards/EthereumWhalewatch.js
@@ -21,7 +21,7 @@ import { useToast } from "../../core/hooks";
 import { useSubscriptions } from "../../core/hooks";
 import moment from "moment";
 import { AiOutlineMonitor } from "react-icons/ai";
-import NewSubscription from "../NewModalSubscripton";
+import NewSubscription from "../NewSubscription";
 
 const EthereumWhalewatchCard_ = ({
   entry,

From 436f87020f82b12cd6f659bd867f52ebbe8bb254 Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Wed, 8 Sep 2021 13:20:19 +0300
Subject: [PATCH 07/14] Add sorting for subscriptions.

---
 frontend/src/components/NewSubscription.js | 52 +++++++++++-----------
 1 file changed, 27 insertions(+), 25 deletions(-)

diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js
index 259f8319..e921e773 100644
--- a/frontend/src/components/NewSubscription.js
+++ b/frontend/src/components/NewSubscription.js
@@ -57,8 +57,6 @@ const _NewSubscription = ({
     onChange: setsubscriptionAdressFormatRadio,
   });
 
-
-
   useEffect(() => {
     if (setIsLoading) {
       setIsLoading(createSubscription.isLoading);
@@ -126,30 +124,34 @@ const _NewSubscription = ({
             flexWrap="wrap"
             position="relative"
           >
-            {typesCache.data.map((type) => {
-              const radio = getRadioProps({
-                value: type.id,
-                isDisabled:
-                  (initialAddress && initialType) ||
-                  !type.active ||
-                  (isFreeOption && type.id !== "ethereum_blockchain"),
-              });
+            {typesCache.data
+              .sort((a, b) =>
+                a?.name > b?.name ? 1 : b?.name > a?.name ? -1 : 0
+              )
+              .map((type) => {
+                const radio = getRadioProps({
+                  value: type.id,
+                  isDisabled:
+                    (initialAddress && initialType) ||
+                    !type.active ||
+                    (isFreeOption && type.id !== "ethereum_blockchain"),
+                });
 
-              return (
-                <RadioCard
-                  px="8px"
-                  py="4px"
-                  mt="2px"
-                  w="190px"
-                  {...radio}
-                  key={`subscription_type_${type.id}`}
-                  label={type.description}
-                  iconURL={type.icon_url}
-                >
-                  {type.name.slice(9, type.name.length)}
-                </RadioCard>
-              );
-            })}
+                return (
+                  <RadioCard
+                    px="8px"
+                    py="4px"
+                    mt="2px"
+                    w="190px"
+                    {...radio}
+                    key={`subscription_type_${type.id}`}
+                    label={type.description}
+                    iconURL={type.icon_url}
+                  >
+                    {type.name.slice(9, type.name.length)}
+                  </RadioCard>
+                );
+              })}
           </Stack>
         </Stack>
 

From 134b785c9ecb8cd0fa150c1a8c742d0136d8924b Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Wed, 8 Sep 2021 15:14:52 +0300
Subject: [PATCH 08/14] Black formating.

---
 backend/moonstream/actions.py      | 7 +++++--
 backend/moonstream/routes/users.py | 3 ++-
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/backend/moonstream/actions.py b/backend/moonstream/actions.py
index c80c623f..1c786e9b 100644
--- a/backend/moonstream/actions.py
+++ b/backend/moonstream/actions.py
@@ -1,6 +1,5 @@
 import json
 import logging
-
 from typing import Optional, Dict, Any
 from enum import Enum
 import uuid
@@ -158,7 +157,11 @@ def create_onboarding_resource(
     token: uuid.UUID,
     resource_data: Dict[str, Any] = {
         "type": data.USER_ONBOARDING_STATE,
-        "steps": {"welcome": 0, "subscriptions": 0, "stream": 0,},
+        "steps": {
+            "welcome": 0,
+            "subscriptions": 0,
+            "stream": 0,
+        },
         "is_complete": False,
     },
 ) -> BugoutResource:
diff --git a/backend/moonstream/routes/users.py b/backend/moonstream/routes/users.py
index 58b44a2f..19cefc8f 100644
--- a/backend/moonstream/routes/users.py
+++ b/backend/moonstream/routes/users.py
@@ -180,7 +180,8 @@ async def logout_handler(request: Request) -> uuid.UUID:
 
 @app.post("/onboarding", tags=["users"], response_model=data.OnboardingState)
 async def set_onboarding_state(
-    request: Request, onboarding_data: data.OnboardingState = Body(...),
+    request: Request,
+    onboarding_data: data.OnboardingState = Body(...),
 ) -> data.OnboardingState:
 
     token = request.state.token

From b9d9584bfeea2732af268fac133d5e4db04e14ee Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Wed, 8 Sep 2021 18:06:12 +0300
Subject: [PATCH 09/14] Add fixes.

---
 backend/moonstream/admin/subscription_types.py | 2 +-
 backend/moonstream/data.py                     | 2 +-
 backend/moonstream/providers/bugout.py         | 1 -
 3 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py
index 51e9976a..f07b2d95 100644
--- a/backend/moonstream/admin/subscription_types.py
+++ b/backend/moonstream/admin/subscription_types.py
@@ -76,8 +76,8 @@ def create_subscription_type(
     id: str,
     name: str,
     description: str,
-    choices: Optional[List[str]],
     icon_url: str,
+    choices: Optional[List[str]] = [],
     stripe_product_id: Optional[str] = None,
     stripe_price_id: Optional[str] = None,
     active: bool = False,
diff --git a/backend/moonstream/data.py b/backend/moonstream/data.py
index df4b6537..d3c0f07b 100644
--- a/backend/moonstream/data.py
+++ b/backend/moonstream/data.py
@@ -12,7 +12,7 @@ class SubscriptionTypeResourceData(BaseModel):
     id: str
     name: str
     description: str
-    choices: Optional[List[str]]
+    choices: List[str] = Field(default_factory=list)
     icon_url: str
     stripe_product_id: Optional[str] = None
     stripe_price_id: Optional[str] = None
diff --git a/backend/moonstream/providers/bugout.py b/backend/moonstream/providers/bugout.py
index 5d7ef530..020733be 100644
--- a/backend/moonstream/providers/bugout.py
+++ b/backend/moonstream/providers/bugout.py
@@ -19,7 +19,6 @@ from ..stream_queries import StreamQuery
 
 logger = logging.getLogger(__name__)
 logger.setLevel(logging.WARN)
-allowed_tags = ["nfts"]
 
 
 class BugoutEventProviderError(Exception):

From 64d67556cfef508020f362ab3cf2d904c6b34433 Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Wed, 8 Sep 2021 18:43:34 +0300
Subject: [PATCH 10/14] Add mypy fixes.

---
 backend/moonstream/admin/subscription_types.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py
index f07b2d95..bee40e00 100644
--- a/backend/moonstream/admin/subscription_types.py
+++ b/backend/moonstream/admin/subscription_types.py
@@ -139,8 +139,8 @@ def cli_create_subscription_type(args: argparse.Namespace) -> None:
         args.id,
         args.name,
         args.description,
-        args.choices,
         args.icon,
+        args.choices,
         args.stripe_product_id,
         args.stripe_price_id,
         args.active,
@@ -371,8 +371,8 @@ def ensure_canonical_subscription_types() -> BugoutResources:
                 id,
                 canonical_subscription_type.name,
                 canonical_subscription_type.description,
-                canonical_subscription_type.choices,
                 canonical_subscription_type.icon_url,
+                canonical_subscription_type.choices,
                 canonical_subscription_type.stripe_product_id,
                 canonical_subscription_type.stripe_price_id,
                 canonical_subscription_type.active,

From 6c68a21faf31b3349db5ddb3ba528c3d537de6ef Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Wed, 8 Sep 2021 20:17:47 +0300
Subject: [PATCH 11/14] Add fixes in labeling staff.

---
 frontend/src/components/NewSubscription.js   |  5 ++---
 frontend/src/components/SubscriptionsList.js | 11 ++++++++++-
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js
index e921e773..c4bd077d 100644
--- a/frontend/src/components/NewSubscription.js
+++ b/frontend/src/components/NewSubscription.js
@@ -73,11 +73,11 @@ const _NewSubscription = ({
     (props) => {
       props.label = "Address";
       if (
-        subscriptionAdressFormatRadio.startsWith("tags") &&
+        subscriptionAdressFormatRadio.startsWith("tag") &&
         radioState != "ethereum_whalewatch"
       ) {
         props.address = subscriptionAdressFormatRadio;
-        props.label = "tags: NFTs";
+        props.label = "Tag";
       }
 
       createSubscription.mutate({
@@ -165,7 +165,6 @@ const _NewSubscription = ({
               (addition_selects) => {
                 const radio = getRadioPropsSubscription({
                   value: addition_selects,
-                  isDisabled: addition_selects.startsWith("tag"),
                 });
                 return (
                   <RadioCard
diff --git a/frontend/src/components/SubscriptionsList.js b/frontend/src/components/SubscriptionsList.js
index 13b50dc5..d398891f 100644
--- a/frontend/src/components/SubscriptionsList.js
+++ b/frontend/src/components/SubscriptionsList.js
@@ -21,6 +21,11 @@ import { useSubscriptions } from "../core/hooks";
 import ConfirmationRequest from "./ConfirmationRequest";
 import ColorSelector from "./ColorSelector";
 
+const mapper = {
+  "tag:nfts": "NFTs",
+  "input:address": "Address",
+};
+
 const SubscriptionsList = ({ emptyCTA }) => {
   const {
     subscriptionsCache,
@@ -94,7 +99,11 @@ const SubscriptionsList = ({ emptyCTA }) => {
                   </Editable>
                 </Td>
                 <Td mr={4} p={0}>
-                  <CopyButton>{subscription.address}</CopyButton>
+                  {subscription.address?.startsWith("tag") ? (
+                    <CopyButton>{mapper[subscription.address]}</CopyButton>
+                  ) : (
+                    <CopyButton>{subscription.address}</CopyButton>
+                  )}
                 </Td>
                 <Td>
                   <ColorSelector

From 3841b9104a81157c76b34a9aaf3be7c019bf5956 Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Wed, 8 Sep 2021 20:41:25 +0300
Subject: [PATCH 12/14] Change tag:nfts -> ERC721.

---
 backend/moonstream/admin/subscription_types.py | 4 ++--
 frontend/src/components/NewSubscription.js     | 2 +-
 frontend/src/components/SubscriptionsList.js   | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py
index bee40e00..ad45f004 100644
--- a/backend/moonstream/admin/subscription_types.py
+++ b/backend/moonstream/admin/subscription_types.py
@@ -20,7 +20,7 @@ CANONICAL_SUBSCRIPTION_TYPES = {
     "ethereum_blockchain": SubscriptionTypeResourceData(
         id="ethereum_blockchain",
         name="Ethereum transactions",
-        choices=["input:address", "tag:nfts"],
+        choices=["input:address", "tag:erc721"],
         description="Transactions that have been mined into the Ethereum blockchain",
         icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-purple.png",
         stripe_product_id=None,
@@ -42,7 +42,7 @@ CANONICAL_SUBSCRIPTION_TYPES = {
         id="ethereum_txpool",
         name="Ethereum transaction pool",
         description="Transactions that have been submitted into the Ethereum transaction pool but not necessarily mined yet",
-        choices=["input:address", "tag:nfts"],
+        choices=["input:address", "tag:erc721"],
         icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-rainbow.png",
         stripe_product_id=None,
         stripe_price_id=None,
diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js
index c4bd077d..5202445b 100644
--- a/frontend/src/components/NewSubscription.js
+++ b/frontend/src/components/NewSubscription.js
@@ -38,7 +38,7 @@ const _NewSubscription = ({
   );
 
   const mapper = {
-    "tag:nfts": "NFTs",
+    "tag:erc721": "NFTs",
     "input:address": "Address",
   };
 
diff --git a/frontend/src/components/SubscriptionsList.js b/frontend/src/components/SubscriptionsList.js
index d398891f..ef1b006e 100644
--- a/frontend/src/components/SubscriptionsList.js
+++ b/frontend/src/components/SubscriptionsList.js
@@ -22,7 +22,7 @@ import ConfirmationRequest from "./ConfirmationRequest";
 import ColorSelector from "./ColorSelector";
 
 const mapper = {
-  "tag:nfts": "NFTs",
+  "tag:erc721": "NFTs",
   "input:address": "Address",
 };
 

From 1199d173c1995cde202b5d2e1edfc9c84021a5a9 Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Thu, 9 Sep 2021 14:08:43 +0300
Subject: [PATCH 13/14] Disable NFTs.

---
 frontend/src/components/NewSubscription.js | 1 +
 1 file changed, 1 insertion(+)

diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js
index 5202445b..7204e775 100644
--- a/frontend/src/components/NewSubscription.js
+++ b/frontend/src/components/NewSubscription.js
@@ -165,6 +165,7 @@ const _NewSubscription = ({
               (addition_selects) => {
                 const radio = getRadioPropsSubscription({
                   value: addition_selects,
+                  isDisabled: addition_selects.startsWith("tag"),
                 });
                 return (
                   <RadioCard

From 69226ebd32b836c1cd279e393f52f872ca97e04b Mon Sep 17 00:00:00 2001
From: Andrey Dolgolev <andrey@simiotics.com>
Date: Thu, 9 Sep 2021 15:05:38 +0300
Subject: [PATCH 14/14] Remove optional.

---
 backend/moonstream/admin/subscription_types.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py
index ad45f004..e666aebf 100644
--- a/backend/moonstream/admin/subscription_types.py
+++ b/backend/moonstream/admin/subscription_types.py
@@ -77,7 +77,7 @@ def create_subscription_type(
     name: str,
     description: str,
     icon_url: str,
-    choices: Optional[List[str]] = [],
+    choices: List[str] = [],
     stripe_product_id: Optional[str] = None,
     stripe_price_id: Optional[str] = None,
     active: bool = False,