diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,3 @@
+# Changelog for monad-metrics-extensible
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Georg Rudoy (c) 2019
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Georg Rudoy nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,1 @@
+# monad-metrics-extensible
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/monad-metrics-extensible.cabal b/monad-metrics-extensible.cabal
new file mode 100644
--- /dev/null
+++ b/monad-metrics-extensible.cabal
@@ -0,0 +1,75 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.31.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: bb9eb3e4722acb9d4fdca6657d73f6bd0989b7a3a4b7039ab5d09b4304702e73
+
+name:           monad-metrics-extensible
+version:        0.1.0.0
+synopsis:       An extensible and type-safe wrapper around EKG metrics
+description:    Please see the README on GitHub at <https://github.com/0xd34df00d/monad-metrics-extensible#readme>
+category:       Web
+homepage:       https://github.com/0xd34df00d/monad-metrics-extensible#readme
+bug-reports:    https://github.com/0xd34df00d/monad-metrics-extensible/issues
+author:         Georg Rudoy
+maintainer:     0xd34df00d@gmail.com
+copyright:      2019 Georg Rudoy
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://github.com/0xd34df00d/monad-metrics-extensible
+
+library
+  exposed-modules:
+      Data.Dyn
+      System.Metrics.Extensible
+      System.Metrics.Monad
+      System.Metrics.Monad.Class
+      System.Metrics.Store
+      System.Metrics.TrackerInstances
+  other-modules:
+      Paths_monad_metrics_extensible
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      base >=4.7 && <5
+    , dependent-map
+    , dependent-sum
+    , ekg
+    , ekg-core
+    , exceptions
+    , mtl
+    , stm
+    , text
+  default-language: Haskell2010
+
+test-suite monad-metrics-extensible-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_monad_metrics_extensible
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , dependent-map
+    , dependent-sum
+    , ekg
+    , ekg-core
+    , exceptions
+    , hspec
+    , monad-metrics-extensible
+    , mtl
+    , stm
+    , text
+  default-language: Haskell2010
diff --git a/src/Data/Dyn.hs b/src/Data/Dyn.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Dyn.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE RankNTypes, GADTs, ConstraintKinds, FlexibleInstances #-}
+
+module Data.Dyn where
+
+import Type.Reflection
+
+data Dyn ctx where
+  Dyn :: ctx a => TypeRep a -> a -> Dyn ctx
+
+toDyn :: (Typeable a, ctx a) => a -> Dyn ctx
+toDyn val = Dyn (typeOf val) val
+
+withDyns :: Dyn ctx -> Dyn ctx ->
+            (forall a. ctx a => a -> a -> b) ->
+            (SomeTypeRep -> SomeTypeRep -> b) -> b
+withDyns (Dyn ty1 v1) (Dyn ty2 v2) f def = case eqTypeRep ty1 ty2 of
+  Nothing -> def (SomeTypeRep ty1) (SomeTypeRep ty2)
+  Just HRefl -> f v1 v2
+
+instance Eq (Dyn Ord) where
+  d1 == d2 = withDyns d1 d2 (==) (\_ _ -> False)
+
+instance Ord (Dyn Ord) where
+  compare d1 d2 = withDyns d1 d2 compare compare
diff --git a/src/System/Metrics/Extensible.hs b/src/System/Metrics/Extensible.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Metrics/Extensible.hs
@@ -0,0 +1,13 @@
+module System.Metrics.Extensible
+( module X
+) where
+
+import System.Metrics.Counter as X(Counter)
+import System.Metrics.Distribution as X(Distribution)
+import System.Metrics.Gauge as X(Gauge)
+import System.Metrics.Label as X(Label)
+
+import System.Metrics.Monad as X
+import System.Metrics.Monad.Class as X
+import System.Metrics.Store as X
+import System.Metrics.TrackerInstances as X
diff --git a/src/System/Metrics/Monad.hs b/src/System/Metrics/Monad.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Metrics/Monad.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE PolyKinds, FlexibleInstances, MultiParamTypeClasses, UndecidableInstances #-}
+
+module System.Metrics.Monad
+( MonadMetrics(..)
+, MetricsT
+, runMetricsT
+, Metrics
+) where
+
+import Control.Monad.Catch
+import Control.Monad.IO.Class
+import Control.Monad.Reader
+
+import System.Metrics.Monad.Class
+import System.Metrics.Store
+
+newtype MetricsT (m :: k -> *) (a :: k) = MetricsT { runMetricsT :: MetricsStore -> m a }
+type Metrics = MetricsT IO
+
+instance Functor m => Functor (MetricsT m) where
+  fmap f (MetricsT m) = MetricsT $ fmap f . m
+
+instance Applicative m => Applicative (MetricsT m) where
+  pure = MetricsT . const . pure
+  (MetricsT fun) <*> (MetricsT val) = MetricsT $ \store -> fun store <*> val store
+
+instance Monad m => Monad (MetricsT m) where
+  (MetricsT val) >>= f = MetricsT $ \store -> val store >>= \a -> runMetricsT (f a) store
+
+instance MonadIO m => MonadMetrics (MetricsT m) where
+  getTracker metric = MetricsT $ \store -> liftIO $ getMetricFromStore store metric
+
+
+instance MonadTrans MetricsT where
+  lift m = MetricsT $ const m
+
+
+instance MonadIO m => MonadIO (MetricsT m) where
+  liftIO act = MetricsT $ const $ liftIO act
+
+instance MonadReader r m => MonadReader r (MetricsT m) where
+  ask = MetricsT $ const ask
+  reader f = MetricsT $ const $ reader f
+  local m (MetricsT rFun) = MetricsT $ local m . rFun
+
+instance MonadThrow m => MonadThrow (MetricsT m) where
+  throwM ex = MetricsT $ const $ throwM ex
+
+instance MonadCatch m => MonadCatch (MetricsT m) where
+  catch (MetricsT act) handler = MetricsT $ \store -> catch (act store) $ \ex -> runMetricsT (handler ex) store
diff --git a/src/System/Metrics/Monad/Class.hs b/src/System/Metrics/Monad/Class.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Metrics/Monad/Class.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE TypeFamilyDependencies #-}
+
+module System.Metrics.Monad.Class where
+
+import qualified Data.Text as T
+import Control.Monad.IO.Class
+import GHC.TypeLits
+import System.Metrics
+import Type.Reflection
+
+class Typeable tracker => TrackerLike tracker where
+  type TrackAction tracker (m :: * -> *) = r | r -> m
+  track :: (MonadMetrics m, KnownSymbol name, Typeable metric, Ord (metric tracker name)) => metric tracker name -> TrackAction tracker m
+  createTracker :: T.Text -> Store -> IO tracker
+
+class MonadIO m => MonadMetrics m where
+  getTracker :: (TrackerLike tracker, KnownSymbol name, Typeable metric, Ord (metric tracker name))
+             => metric tracker name -> m tracker
diff --git a/src/System/Metrics/Store.hs b/src/System/Metrics/Store.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Metrics/Store.hs
@@ -0,0 +1,105 @@
+{-# LANGUAGE RankNTypes, GADTs #-}
+{-# LANGUAGE StandaloneDeriving, ScopedTypeVariables, TypeOperators #-}
+
+module System.Metrics.Store
+( MetricsStore
+, newMetricsStore
+, withMetricsStore
+
+, getMetricFromStore
+) where
+
+import qualified Data.Dependent.Map as DM
+import qualified Data.Text as T
+import Control.Concurrent
+import Control.Concurrent.STM.TQueue
+import Control.Monad.Catch
+import Control.Monad.Identity
+import Control.Monad.STM
+import Data.GADT.Compare
+import Data.Proxy
+import Data.Typeable(eqT)
+import GHC.TypeLits
+import System.Remote.Monitoring
+import Type.Reflection
+
+import Data.Dyn
+import System.Metrics.Monad.Class
+
+type DynOrd = Dyn Ord
+
+data SomeMetric tracker where
+  MkSomeMetric :: (Typeable metric, TrackerLike tracker, KnownSymbol name, Ord (metric tracker name))
+               => metric tracker name -> SomeMetric tracker
+
+deriving instance Typeable (SomeMetric t)
+
+instance GEq SomeMetric where
+  geq (MkSomeMetric _) (MkSomeMetric _) = eqT
+
+instance GCompare SomeMetric where
+  gcompare sm1@(MkSomeMetric (m1 :: mTy1 tTy1 nTy1)) sm2@(MkSomeMetric (m2 :: mTy2 tTy2 nTy2)) =
+    case eqT :: Maybe (tTy1 :~: tTy2) of
+      Just Refl -> case compare (toDyn m1 :: DynOrd) (toDyn m2) of
+        LT -> GLT
+        EQ -> GEQ
+        GT -> GGT
+      Nothing -> case compare (someTypeRep sm1) (someTypeRep sm2) of
+        LT -> GLT
+        EQ -> error "SomeTypeReps are equal though eqT proved them wrong"
+        GT -> GGT
+
+data MetricsState = MetricsState
+  { server :: Server
+  , metrics :: DM.DMap SomeMetric Identity
+  }
+
+data MetricRequest where
+  MetricRequest :: (TrackerLike tracker, KnownSymbol name, Typeable metric, Ord (metric tracker name))
+                => metric tracker name
+                -> MVar tracker
+                -> MetricRequest
+
+newtype MetricsStore = MetricsStore { mReqQueue :: TQueue MetricRequest }
+
+newMetricsStore :: Server -> IO (MetricsStore, IO ())
+newMetricsStore srv = do
+  queue <- newTQueueIO
+  threadId <- forkIO $ act queue $ MetricsState srv mempty
+  pure (MetricsStore queue, killThread threadId)
+  where
+    act queue state = do
+      req <- atomically (readTQueue queue)
+      state' <- (\(MetricRequest metric mvar) -> handleReq state metric mvar) req
+      act queue state'
+
+    handleReq :: forall metric tracker name. (TrackerLike tracker, KnownSymbol name, Typeable metric, Ord (metric tracker name))
+              => MetricsState
+              -> metric tracker name
+              -> MVar tracker
+              -> IO MetricsState
+    handleReq state metric mvar = do
+      let asSome = MkSomeMetric metric
+      (tracker, state') <- case DM.lookup asSome $ metrics state of
+        Just existing -> pure (runIdentity existing, state)
+        Nothing -> do
+          let trackerName = symbolVal (Proxy :: Proxy name)
+          newTracker <- createTracker (T.pack trackerName) $ serverMetricStore $ server state
+          pure (newTracker, state { metrics = DM.insert asSome (Identity newTracker) $ metrics state })
+      putMVar mvar tracker
+      pure state'
+
+withMetricsStore :: Server -> (MetricsStore -> IO a) -> IO a
+withMetricsStore srv f = bracket
+  (newMetricsStore srv)
+  snd
+  (f . fst)
+
+getMetricFromStore :: (TrackerLike tracker, KnownSymbol name, Typeable metric, Ord (metric tracker name))
+                   => MetricsStore
+                   -> metric tracker name
+                   -> IO tracker
+getMetricFromStore store metric = do
+  mvar <- newEmptyMVar
+  atomically $ writeTQueue (mReqQueue store) $ MetricRequest metric mvar
+  takeMVar mvar
diff --git a/src/System/Metrics/TrackerInstances.hs b/src/System/Metrics/TrackerInstances.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Metrics/TrackerInstances.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module System.Metrics.TrackerInstances where
+
+import qualified Data.Text as T
+import Control.Monad.IO.Class
+import GHC.Int
+import System.Metrics
+import System.Metrics.Counter as TC
+import System.Metrics.Distribution as TD
+import System.Metrics.Gauge as TG
+import System.Metrics.Label as TL
+
+import System.Metrics.Monad.Class
+
+instance TrackerLike Counter where
+  type TrackAction Counter m = m ()
+  track metric = getTracker metric >>= liftIO . TC.inc
+  createTracker = createCounter
+
+instance TrackerLike Distribution where
+  type TrackAction Distribution m = Double -> m ()
+  track metric val = getTracker metric >>= \distr -> liftIO $ TD.add distr val
+  createTracker = createDistribution
+
+instance TrackerLike Gauge where
+  type TrackAction Gauge m = Int64 -> m ()
+  track metric val = getTracker metric >>= \gauge -> liftIO $ TG.set gauge val
+  createTracker = createGauge
+
+instance TrackerLike Label where
+  type TrackAction Label m = T.Text -> m ()
+  track metric val = getTracker metric >>= \label -> liftIO $ TL.set label val
+  createTracker = createLabel
+
+newtype DistrGauge = DistrGauge (Distribution, Gauge)
+
+instance TrackerLike DistrGauge where
+  type TrackAction DistrGauge m = Int64 -> m ()
+  track metric val = do
+    DistrGauge (distr, gauge) <- getTracker metric
+    liftIO $ do
+      TG.add gauge val
+      TD.add distr $ fromIntegral val
+  createTracker name store = do
+    d <- createDistribution (name <> "_distr") store
+    g <- createGauge (name <> "_total") store
+    pure $ DistrGauge (d, g)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,45 @@
+{-# LANGUAGE DataKinds, GADTs #-}
+{-# LANGUAGE OverloadedStrings, StandaloneDeriving #-}
+
+import Control.Monad.IO.Class
+import System.Metrics.Counter as TC(read)
+import System.Metrics.Gauge as TG(read)
+import System.Remote.Monitoring
+import Test.Hspec
+
+import System.Metrics.Extensible
+
+data TestMetrics ty name where
+  Foo1 :: TestMetrics Counter "foo1"
+  Foo2 :: TestMetrics Counter "foo2"
+  Bar  :: TestMetrics Gauge   "bar"
+
+deriving instance Eq (TestMetrics ty name)
+deriving instance Ord (TestMetrics ty name)
+
+data OtherMetrics ty name where
+  Baz  :: OtherMetrics Counter "foo1"
+  Quux :: OtherMetrics Gauge   "bar"
+
+deriving instance Eq (OtherMetrics ty name)
+deriving instance Ord (OtherMetrics ty name)
+
+main :: IO ()
+main = do
+  ekgServer <- forkServer "localhost" 21000
+  withMetricsStore ekgServer $ \store -> flip runMetricsT store $ do
+    track Foo1
+    track Foo1
+    track Foo2
+    track Bar 10
+    track Bar 20
+    track Baz
+    track Quux 42
+    liftIO $ hspec $ do
+      describe "Counters are stored as expected" $ do
+        it "Foo1 is incremented twice" $ (getMetricFromStore store Foo1 >>= TC.read) `shouldReturn` 2
+        it "This does not affect Foo2" $ (getMetricFromStore store Foo2 >>= TC.read) `shouldReturn` 1
+        it "Metrics from other type are also saved" $ (getMetricFromStore store Baz >>= TC.read) `shouldReturn` 1
+      describe "Gauges are stored as expected" $ do
+        it "The final value of Bar is kept" $ (getMetricFromStore store Bar >>= TG.read) `shouldReturn` 20
+        it "Metrics from other type are also saved" $ (getMetricFromStore store Quux >>= TG.read) `shouldReturn` 42
