diff --git a/datadog-tracing.cabal b/datadog-tracing.cabal
--- a/datadog-tracing.cabal
+++ b/datadog-tracing.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.2
 name:                datadog-tracing
-version:             1.1.0
+version:             1.2.0
 synopsis:            Datadog tracing client and mock agent.
 license:             BSD-3-Clause
 license-file:        LICENSE
@@ -29,6 +29,7 @@
   build-depends:    , base ^>= 4.11.1.0 || ^>= 4.12.0.0
                     , bytestring           ^>= 0.10.8.2
                     , containers ^>= 0.5.11.0 || ^>= 0.6.0.1
+                    , mtl                  ^>= 2.2.2
                     , text                 ^>= 1.2.3.1
                     , aeson                ^>= 1.4.1.0
                     , servant              ^>= 0.14.1
@@ -42,7 +43,8 @@
   exposed-modules:    Datadog.Agent
                     , Datadog.Client
                     , Datadog.Jaeger
-  build-depends:    , generic-random  ^>= 1.2.0.0
+  build-depends:    , ffunctor        ^>= 1.1.0
+                    , generic-random  ^>= 1.2.0.0
                     , refined         ^>= 0.2.3.0
                     , prettyprinter   ^>= 1.2.1
                     , servant-client  ^>= 0.14
diff --git a/library/Datadog/Client.hs b/library/Datadog/Client.hs
--- a/library/Datadog/Client.hs
+++ b/library/Datadog/Client.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE DataKinds             #-}
+{-# LANGUAGE FlexibleContexts      #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE OverloadedStrings     #-}
 {-# LANGUAGE TypeApplications      #-}
@@ -9,10 +10,28 @@
 --
 -- Many of our refinements are stricter than the actual requirements, to err on
 -- the side of caution, and because the actual requirements are very complex.
-module Datadog.Client where
+module Datadog.Client (
+    Agent(..)
+  , AgentT
+  , DDText
+  , HasAlpha
+  , MetaKey(..)
+  , MetaValue(..)
+  , ServiceName(..)
+  , Span(..)
+  , SpanId(..)
+  , SpanName(..)
+  , Tag
+  , Trace(..)
+  , TraceId(..)
+  , newServantAgent
+) where
 
 import           Control.Monad             (unless, void)
+import           Control.Monad.Except      (ExceptT, MonadError, MonadIO,
+                                            liftEither, liftIO)
 import           Data.Char                 (isAlpha, isAsciiLower, isDigit)
+import           Data.FFunctor             (FFunctor, ffmap)
 import           Data.Int                  (Int64)
 import qualified Data.List.NonEmpty        as NEL
 import           Data.Map.Strict           (Map)
@@ -25,26 +44,47 @@
 import           Data.Time.Clock.POSIX     (utcTimeToPOSIXSeconds)
 import           Data.Typeable             (Proxy (..), Typeable, typeOf)
 import           Data.Word                 (Word64)
-import           Refined
-import           Servant.Client            (ClientM, client)
+import           Refined                   hiding (NonEmpty)
+import qualified Refined
+import           Servant.Client            (ClientEnv, ClientM, ServantError,
+                                            client, runClientM)
 
 import qualified Datadog.Agent             as API
 
-type DDText = NonEmpty && (SizeLessThan 101)
+-- | The Datadog Agent API, independent of any HTTP framework.
+newtype Agent m = Agent
+  { putTraces :: NEL.NonEmpty Trace -> m ()
+  }
 
-newtype SpanId = SpanId (Refined NonZero Word64)
-newtype TraceId = TraceId (Refined NonZero Word64)
-newtype ServiceName = ServiceName (Refined (DDText && Tag) Text)
+-- | Allows users to opt-out of having a MonadError in their Monad stack. They
+--   opt-in to error handling at the points when calling the Agent. Requires
+--   users to handle errors at the point of use.
+--
+--   See https://discourse.haskell.org/t/local-capabilities-with-mtl/231
+type AgentT m = Agent (ExceptT ServantError m)
 
+instance FFunctor Agent where
+  ffmap nt (Agent p1) = Agent (nt . p1)
+
+-- | An Agent (or AgentT) implemented by Servant.
+newServantAgent :: (MonadIO m, MonadError ServantError m) => ClientEnv -> Agent m
+newServantAgent env = ffmap (liftClientM env) (Agent traces)
+
+type DDText = Refined.NonEmpty && (SizeLessThan 101)
+
+newtype SpanId = SpanId (Refined NonZero Word64) deriving (Eq, Show)
+newtype TraceId = TraceId (Refined NonZero Word64) deriving (Eq, Show)
+newtype ServiceName = ServiceName (Refined (DDText && Tag) Text) deriving (Eq, Show)
+
 data Trace = Trace
   { tService :: ServiceName
   , tId      :: TraceId
-  , tSpans   :: (Refined NonEmpty (Map SpanId Span))
-  }
+  , tSpans   :: (Refined (Refined.NonEmpty) (Map SpanId Span))
+  } deriving (Eq, Show)
 
-newtype SpanName = SpanName (Refined (DDText && HasAlpha) Text)
-newtype MetaKey = MetaKey (Refined (DDText && Tag) Text) deriving (Eq, Ord)
-newtype MetaValue = MetaValue (Refined DDText Text)
+newtype SpanName = SpanName (Refined (DDText && HasAlpha) Text) deriving (Eq, Show)
+newtype MetaKey = MetaKey (Refined (DDText && Tag) Text) deriving (Eq, Ord, Show)
+newtype MetaValue = MetaValue (Refined DDText Text) deriving (Eq, Show)
 
 data Span = Span
   { sName     :: SpanName
@@ -52,7 +92,7 @@
   , sStart    :: UTCTime
   , sDuration :: NominalDiffTime
   , sMeta     :: Maybe (Map MetaKey MetaValue)
-  }
+  } deriving (Eq, Show)
 
 traces :: NEL.NonEmpty Trace -> ClientM ()
 traces (NEL.toList -> ts) = void . raw $ toAPI <$> ts
@@ -117,3 +157,10 @@
 validate' t p a =
   unless (p a) $
   throwRefineOtherException (typeOf t) ("failed predicate: " <> (viaShow a))
+
+-- | Converts ClientM signatures into MTL, pushing errors into the stack.
+liftClientM :: (MonadIO m, MonadError ServantError m)
+  => ClientEnv
+  -> ClientM a
+  -> m a
+liftClientM env ca = liftEither =<< (liftIO $ runClientM ca env)
