diff --git a/craze.cabal b/craze.cabal
--- a/craze.cabal
+++ b/craze.cabal
@@ -1,9 +1,9 @@
--- This file has been generated from package.yaml by hpack version 0.9.0.
+-- This file has been generated from package.yaml by hpack version 0.14.0.
 --
 -- see: https://github.com/sol/hpack
 
 name:                craze
-version:             0.1.2.0
+version:             0.1.3.0
 synopsis:            HTTP Racing Library
 description:         A micro-library for racing HTTP GET requests
 homepage:            https://github.com/etcinit/craze#readme
@@ -29,11 +29,19 @@
     , bytestring
     , transformers
     , text
+    , mtl
+    , lifted-async
+    , lifted-base
+    , monad-control
+    , transformers-base
+    , lens
     , async
     , data-default-class
     , containers
   exposed-modules:
       Network.Craze
+      Network.Craze.Internal
+      Network.Craze.Types
   default-language: Haskell2010
 
 executable craze-example
@@ -47,13 +55,19 @@
     , bytestring
     , transformers
     , text
+    , mtl
+    , lifted-async
+    , lifted-base
+    , monad-control
+    , transformers-base
+    , lens
     , craze
     , optparse-generic
   default-language: Haskell2010
 
-test-suite craze-spec
+test-suite craze-doctest
   type: exitcode-stdio-1.0
-  main-is: Spec.hs
+  main-is: Doctest.hs
   hs-source-dirs:
       test
   ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -Wall -fno-warn-unused-do-bind
@@ -63,20 +77,23 @@
     , bytestring
     , transformers
     , text
+    , mtl
+    , lifted-async
+    , lifted-base
+    , monad-control
+    , transformers-base
+    , lens
     , craze
-    , hspec
-    , hspec-discover
-    , http-types
-    , haxy
-    , HTTP
+    , doctest
+    , doctest-discover
   other-modules:
-      Doctest
       Network.CrazeSpec
+      Spec
   default-language: Haskell2010
 
-test-suite craze-doctest
+test-suite craze-spec
   type: exitcode-stdio-1.0
-  main-is: Doctest.hs
+  main-is: Spec.hs
   hs-source-dirs:
       test
   ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -Wall -fno-warn-unused-do-bind
@@ -86,10 +103,19 @@
     , bytestring
     , transformers
     , text
+    , mtl
+    , lifted-async
+    , lifted-base
+    , monad-control
+    , transformers-base
+    , lens
     , craze
-    , doctest
-    , doctest-discover
+    , hspec
+    , hspec-discover
+    , http-types
+    , haxy
+    , HTTP
   other-modules:
+      Doctest
       Network.CrazeSpec
-      Spec
   default-language: Haskell2010
diff --git a/src/Network/Craze.hs b/src/Network/Craze.hs
--- a/src/Network/Craze.hs
+++ b/src/Network/Craze.hs
@@ -1,22 +1,25 @@
-{-# LANGUAGE FlexibleInstances  #-}
-{-# LANGUAGE GADTs              #-}
-{-# LANGUAGE ImpredicativeTypes #-}
-{-# LANGUAGE OverloadedStrings  #-}
-{-# LANGUAGE RankNTypes         #-}
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE GADTs                 #-}
+{-# LANGUAGE ImpredicativeTypes    #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE RankNTypes            #-}
+{-# LANGUAGE RecordWildCards       #-}
 
 -- | Craze is a small module for performing multiple similar HTTP GET requests
 -- in parallel. This is performed through the `raceGet` function, which will
 -- perform all the requests and pick the first successful response that passes
--- a certain check, menaing that the parallel requests are essentially racing
+-- a certain check, meaning that the parallel requests are essentially racing
 -- against each other.
 --
 -- __What is the usefulness of this?__
 --
 -- If you are dealing with data source or API that is very unreliable (high
--- latency, random failures) and there are no limitations or consequences on
--- perfoming significantly more requests, then performing multiple requests
--- (through direct connections, proxies, VPNs) may increase the chances of
--- getting a successful response faster and more reliably.
+-- latency, random failures) and there are no limitations on performing
+-- significantly more requests, then performing multiple requests (through
+-- direct connections, proxies, VPNs) may increase the chances of getting a
+-- successful response faster and more reliably.
 --
 -- However, if using a different data source or transport is a possibility, it
 -- is potentially a better option that this approach.
@@ -26,11 +29,17 @@
 -- Performing two parallel GET requests against https://chromabits.com and
 -- returning the status code of the first successful one:
 --
+-- The providers generate two client configurations. The handler "parses" the
+-- response (in this case it just gets the status code). Finally, the checker
+-- filters out responses that we don't consider valid (anything that is not
+-- HTTP 200 in this case).
+--
+-- >>> :set -XOverloadedStrings
 -- >>> :{
 --  let racer = (Racer
 --                { racerProviders =
---                    [ return defaultProviderOptions
---                    , return defaultProviderOptions
+--                    [ simpleTagged [] "Client A"
+--                    , simpleTagged [] "Client B"
 --                    ]
 --                , racerHandler = return . respStatus
 --                , racerChecker = (200 ==)
@@ -46,150 +55,39 @@
     RacerHandler
   , RacerChecker
   , Racer(..)
+  , RacerProvider
   , ProviderOptions(..)
-  , RacerResult
+  , RacerResult(..)
+  , ClientStatus(..)
   -- * Functions
-  , defaultRacer
-  , defaultProviderOptions
   , raceGet
   , raceGetResult
   -- * Providers
+  -- $providers
   , simple
   , simpleTagged
   , delayed
   , delayedTagged
-  -- * Results
-  , rrResponse
-  , rrWinner
-  , rrProviders
+  -- * Deprecated
+  , defaultRacer
+  , defaultProviderOptions
   ) where
 
-import           Control.Concurrent       (threadDelay)
+import Control.Monad (when)
+import Data.Map.Lazy (keys, lookup)
+import Prelude       hiding (lookup)
+
 import           Control.Concurrent.Async
-import           Control.Monad            (when)
+import           Control.Monad.State      (runStateT)
+import           Control.Monad.Trans      (MonadIO, liftIO)
 import           Data.ByteString          (ByteString)
-import           Data.Default.Class       (Default, def)
-import           Data.Map.Lazy            (Map, delete, elems, fromList, keys,
-                                           lookup, mapWithKey)
-import           Data.Monoid              ((<>))
+import           Data.Default.Class       (def)
 import           Data.Text                (Text, pack)
 import qualified Data.Text.IO             as TIO
 import           Network.Curl
-import           Prelude                  hiding (lookup)
 
--- | A `RacerHandler` is simply a function for transforming a response after it
--- is received. The handler is only applied to successful requests before they
--- are checked by the `RacerChecker`.
---
--- This is primarily for extracting or parsing a `CurlResponse_` before doing
--- any further work. The type returned by the handler will be used as the
--- input of the checker and will be the return type of `raceGet`.
-type RacerHandler headerTy bodyTy a = CurlResponse_ headerTy bodyTy -> IO a
-
--- | A function that computes whether or not a result is valid or not.
--- Successful responses that do not pass the checker are discarded.
---
--- This should help filtering out successful responses that do not, for some
--- reason, have the expected result (e.g. Random content changes, Rate
--- Limitting, etc).
-type RacerChecker a = a -> Bool
-
--- | A function that returns the @ProviderOptions@ to use for making a request.
-type RacerProvider = IO ProviderOptions
-
--- | Options for a specific provider.
-data ProviderOptions = ProviderOptions
-  { -- | Options to pass down to Curl.
-    poOptions :: [CurlOption]
-    -- | Number of microseconds to delay the request by.
-  , poDelay   :: Maybe Int
-    -- | A tag to identify this type provider.
-  , poTag     :: Text
-  } deriving (Show)
-
-instance Default ProviderOptions where
-  def = ProviderOptions
-    { poOptions = []
-    , poDelay = Nothing
-    , poTag = "default"
-    }
-
--- | The result of a racing operation. This can be used to collect statistics
--- on which providers win more often, etc.
-data RacerResult a = RacerResult
-  { rrResponse  :: Maybe a
-  , rrWinner    :: Maybe ProviderOptions
-  , rrProviders :: [RacerProvider]
-  }
-
--- | A record describing the rules for racing requests.
-data Racer headerTy bodyTy a = Racer
-  { racerHandler    :: RacerHandler headerTy bodyTy a
-  , racerChecker    :: RacerChecker a
-  -- | On a `Racer`, each `RaceProvider` represents a separate client
-  -- configuration. When performing a race, each provider will be used to spwan
-  -- a client and perform a request. This allows one to control the number of
-  -- requests performed and with which `CurlOption`s.
-  , racerProviders  :: [RacerProvider]
-  -- | When set to `True`, debugging messages will be written to stdout.
-  , racerDebug      :: Bool
-  -- | When set to `True`, the Racer will attempt to return the last response
-  -- in the event that all responses failed to pass the checker. This can be
-  -- used for identifying error conditions.
-  , racerReturnLast :: Bool
-  }
-
-instance Default (Racer [(String,String)] ByteString ByteString) where
-  def = Racer
-    { racerHandler = return . respBody
-    , racerChecker = const True
-    , racerProviders = []
-    , racerDebug = False
-    , racerReturnLast = False
-    }
-
--- | A `Racer` with some default values.
---
--- __Note:__ The handler will extract the response body as a `ByteString` and
--- ignore everything else, hence the type:
---
--- @
--- Racer [(String, String)] ByteString ByteString
--- @
---
--- If this is not the desired behavior, or if the response should be parsed or
--- processed, you should use the `Racer` constructor directly and provide all
--- fields.
-defaultRacer :: Racer [(String,String)] ByteString ByteString
-defaultRacer = def
-
--- | A default set of options for a provider.
-defaultProviderOptions :: ProviderOptions
-defaultProviderOptions = def
-
--- | A simple provider. It does not delay requests.
-simple :: [CurlOption] -> IO ProviderOptions
-simple xs = pure $ def { poOptions = xs }
-
--- | Like @simple@, but with a tag for identification.
-simpleTagged :: [CurlOption] -> Text -> IO ProviderOptions
-simpleTagged xs t = do
-  opts <- simple xs
-  pure $ opts { poTag = t }
-
--- | A provider which will delay a request by the provided number of
--- microseconds.
-delayed :: [CurlOption] -> Int -> IO ProviderOptions
-delayed xs d = pure $ def
-  { poOptions = xs
-  , poDelay = Just d
-  }
-
--- | Like @delayed@, but with a tag for identification.
-delayedTagged :: [CurlOption] -> Int -> Text -> IO ProviderOptions
-delayedTagged xs d t = do
-  opts <- delayed xs d
-  pure $ opts { poTag = t }
+import Network.Craze.Internal
+import Network.Craze.Types
 
 -- | Perform a GET request on the provided URL using all providers in
 -- parallel.
@@ -210,105 +108,91 @@
 --       finish.
 --
 raceGet
-  :: (Eq a, CurlHeader ht, CurlBuffer bt)
+  :: (Eq a, CurlHeader ht, CurlBuffer bt, MonadIO m)
   => Racer ht bt a
   -> URLString
-  -> IO (Maybe a)
+  -> m (Maybe a)
 raceGet r url = rrResponse <$> raceGetResult r url
 
 -- | Same as @raceGet@, but returns a @RacerResult@ which contains more
 -- information about the race performed.
 raceGetResult
-  :: (Eq a, CurlHeader ht, CurlBuffer bt)
+  :: (Eq a, CurlHeader ht, CurlBuffer bt, MonadIO m)
   => Racer ht bt a
   -> URLString
-  -> IO (RacerResult a)
-raceGetResult r url = do
-  asyncs <- fromList <$> mapM performGetAsync_ (racerProviders r)
+  -> m (RacerResult a)
+raceGetResult r@Racer{..} url = do
+  initialState@RaceState{..} <- makeRaceState (pack url) r
 
-  when (racerDebug r) $ do
+  let asyncs = keys _rsClientMap
+
+  when racerDebug . liftIO $ do
     TIO.putStr "[racer] Created Asyncs: "
-    print . elems $ mapWithKey identifier asyncs
+    print $ asyncThreadId <$> asyncs
 
-  maybeResponse <- waitForOne
-    asyncs (racerHandler r) (racerChecker r) (racerDebug r) (racerReturnLast r)
+  (maybeResponse, finalState) <- runStateT waitForOne initialState
 
   pure $ case maybeResponse of
     Nothing -> RacerResult
       { rrResponse = Nothing
       , rrWinner = Nothing
-      , rrProviders = racerProviders r
+      , rrProviders = racerProviders
+      , rrStatuses = extractStatuses finalState
       }
     Just (as, response) -> RacerResult
       { rrResponse = Just response
-      , rrWinner = lookup as asyncs
-      , rrProviders = racerProviders r
+      , rrWinner = _csOptions <$> lookup as _rsClientMap
+      , rrProviders = racerProviders
+      , rrStatuses = extractStatuses finalState
       }
-    where
-      performGetAsync_
-        :: (CurlHeader ht, CurlBuffer bt)
-        => RacerProvider
-        -> IO (Async (CurlResponse_ ht bt), ProviderOptions)
-      performGetAsync_ = performGetAsync url
 
-waitForOne
-  :: (Eq a)
-  => Map (Async (CurlResponse_ ht bt)) ProviderOptions
-  -> RacerHandler ht bt a
-  -> RacerChecker a
-  -> Bool
-  -> Bool
-  -> IO (Maybe (Async (CurlResponse_ ht bt), a))
-waitForOne asyncs handler check debug returnLast
-  = if null asyncs then pure Nothing else do
-    winner <- waitAnyCatch (keys asyncs)
-
-    case winner of
-      (as, Right a) -> do
-        result <- handler a
-
-        let remaining = delete as asyncs
-
-        if check result then do
-          cancelAll (keys remaining)
-
-          when debug $ do
-            TIO.putStr "[racer] Winner: "
-            print (asyncThreadId as)
-
-          pure $ Just (as, result)
-          else (if returnLast && null remaining
-            then do
-              when debug $ do
-                TIO.putStr "[racer] Reached last. Returning: "
-                print (asyncThreadId as)
-
-              pure $ Just (as, result)
-            else waitForOne remaining handler check debug returnLast
-          )
-      (as, Left _) -> waitForOne
-        (delete as asyncs) handler check debug returnLast
-
-cancelAll :: [Async a] -> IO ()
-cancelAll = mapM_ (async . cancel)
+-- $providers
+--
+-- 'RacerProvider' provide client configurations. Craze comes bundled with a
+-- few built-in providers which can be used for quickly building client
+-- configurations.
 
-identifier :: Async (CurlResponse_ ht bt) -> ProviderOptions -> Text
-identifier a o = poTag o <> ":" <> (pack . show . asyncThreadId $ a)
+-- | A simple provider. It does not delay requests.
+simple :: Monad m => [CurlOption] -> m ProviderOptions
+simple xs = pure $ def { poOptions = xs }
 
-performGetAsync
-  :: (CurlHeader ht, CurlBuffer bt)
-  => URLString
-  -> RacerProvider
-  -> IO (Async (CurlResponse_ ht bt), ProviderOptions)
-performGetAsync url provider = do
-  options <- provider
+-- | Like @simple@, but with a tag for identification.
+simpleTagged :: Monad m => [CurlOption] -> Text -> m ProviderOptions
+simpleTagged xs t = do
+  opts <- simple xs
+  pure $ opts { poTag = t }
 
-  responseAsync <- async $ do
-    case poDelay options of
-      Nothing -> pure ()
-      Just delay -> threadDelay delay
+-- | A provider which will delay a request by the provided number of
+-- microseconds.
+delayed :: Monad m => [CurlOption] -> Int -> m ProviderOptions
+delayed xs d = pure $ def
+  { poOptions = xs
+  , poDelay = Just d
+  }
 
-    curlGetResponse_ url (poOptions options)
+-- | Like @delayed@, but with a tag for identification.
+delayedTagged :: Monad m => [CurlOption] -> Int -> Text -> m ProviderOptions
+delayedTagged xs d t = do
+  opts <- delayed xs d
+  pure $ opts { poTag = t }
 
-  pure (responseAsync, options)
+-- | A `Racer` with some default values.
+--
+-- __Note:__ The handler will extract the response body as a `ByteString` and
+-- ignore everything else, hence the type:
+--
+-- @
+-- Racer [(String, String)] ByteString ByteString
+-- @
+--
+-- If this is not the desired behavior, or if the response should be parsed or
+-- processed, you should use the `Racer` constructor directly and provide all
+-- fields.
+defaultRacer :: Racer [(String,String)] ByteString ByteString
+defaultRacer = def
+{-# DEPRECATED defaultRacer "Use Data.Default.Class.def instead" #-}
 
+-- | A default set of options for a provider.
+defaultProviderOptions :: ProviderOptions
+defaultProviderOptions = def
+{-# DEPRECATED defaultProviderOptions "Use Data.Default.Class.def instead" #-}
diff --git a/src/Network/Craze/Internal.hs b/src/Network/Craze/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Craze/Internal.hs
@@ -0,0 +1,201 @@
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE RecordWildCards       #-}
+{-# LANGUAGE TemplateHaskell       #-}
+
+module Network.Craze.Internal where
+
+import           Control.Exception (SomeException)
+import           Control.Monad     (forM, when)
+import           Data.Map.Lazy     (Map)
+import qualified Data.Map.Lazy     as M
+import           Data.Monoid       ((<>))
+
+import           Control.Concurrent.Async.Lifted (Async, async, asyncThreadId,
+                                                  cancel, waitAnyCatch)
+import           Control.Concurrent.Lifted       (threadDelay)
+import           Control.Lens                    (at, makeLenses, use, (&),
+                                                  (.~), (?=))
+import           Control.Monad.State             (MonadState)
+import           Control.Monad.Trans             (MonadIO, liftIO)
+import           Data.Text                       (Text)
+import qualified Data.Text                       as T
+import qualified Data.Text.IO                    as TIO
+import           Network.Curl                    (CurlBuffer, CurlHeader,
+                                                  CurlResponse_,
+                                                  curlGetResponse_)
+
+import Network.Craze.Types
+
+type ClientMap ht bt a = Map (Async (CurlResponse_ ht bt)) (ClientState a)
+
+data ClientState a = ClientState
+  { _csOptions :: ProviderOptions
+  , _csStatus  :: ClientStatus a
+  }
+
+data RaceState ht bt a = RaceState
+  { _rsClientMap  :: ClientMap ht bt a
+  , _rsChecker    :: RacerChecker a
+  , _rsHandler    :: RacerHandler ht bt a
+  , _rsDebug      :: Bool
+  , _rsReturnLast :: Bool
+  }
+
+makeLenses ''ClientState
+makeLenses ''RaceState
+
+extractStatuses :: RaceState ht bt a -> [(Text, ClientStatus a)]
+extractStatuses RaceState{..} = M.elems $ makeTuple <$>  _rsClientMap
+  where
+    makeTuple :: ClientState a -> (Text, ClientStatus a)
+    makeTuple ClientState{..} = (poTag _csOptions, _csStatus)
+
+makeRaceState
+  :: (CurlHeader ht, CurlBuffer bt, MonadIO m)
+  => Text
+  -> Racer ht bt a
+  -> m (RaceState ht bt a)
+makeRaceState url Racer{..} = do
+  providerMap <- makeClientMap url racerProviders
+
+  pure $ RaceState
+    providerMap
+    racerChecker
+    racerHandler
+    racerDebug
+    racerReturnLast
+
+makeClientMap
+  :: (CurlHeader ht, CurlBuffer bt, MonadIO m)
+  => Text
+  -> [RacerProvider]
+  -> m (ClientMap ht bt a)
+makeClientMap url providers = M.fromList <$> forM providers (makeClient url)
+
+makeClient
+  :: (CurlHeader ht, CurlBuffer bt, MonadIO m)
+  => Text
+  -> RacerProvider
+  -> m (Async (CurlResponse_ ht bt), ClientState a)
+makeClient url provider = liftIO $ do
+  options <- provider
+  future <- async $ performGet url options
+
+  pure (future, ClientState options Pending)
+
+performGet
+  :: (CurlHeader ht, CurlBuffer bt)
+  => Text
+  -> ProviderOptions
+  -> IO (CurlResponse_ ht bt)
+performGet url ProviderOptions{..} = do
+  case poDelay of
+    Nothing -> pure ()
+    Just delay -> threadDelay delay
+
+  curlGetResponse_ (T.unpack url) poOptions
+
+cancelAll :: MonadIO m => [Async a] -> m ()
+cancelAll = liftIO . mapM_ (async . cancel)
+
+cancelRemaining
+  :: (MonadIO m, MonadState (RaceState ht bt a) m)
+  => m ()
+cancelRemaining = do
+  remaining <- onlyPending <$> use rsClientMap
+
+  cancelAll $ M.keys remaining
+
+identifier :: Async (CurlResponse_ ht bt) -> ProviderOptions -> Text
+identifier a o = poTag o <> ":" <> (T.pack . show . asyncThreadId $ a)
+
+onlyPending :: ClientMap ht bt a -> ClientMap ht bt a
+onlyPending = M.filter (isPending . _csStatus)
+
+isPending :: ClientStatus a -> Bool
+isPending Pending = True
+isPending _ = False
+
+markAsSuccessful
+  :: (MonadState (RaceState ht bt a) m)
+  => Async (CurlResponse_ ht bt)
+  -> a
+  -> m ()
+markAsSuccessful key result = do
+  maybePrevious <- use $ rsClientMap . at key
+
+  case maybePrevious of
+    Just previous -> (rsClientMap . at key)
+      ?= (previous & csStatus .~ Successful result)
+    Nothing -> pure ()
+
+markAsFailure
+  :: (MonadState (RaceState ht bt a) m)
+  => Async (CurlResponse_ ht bt)
+  -> a
+  -> m ()
+markAsFailure key result = do
+  maybePrevious <- use $ rsClientMap . at key
+
+  case maybePrevious of
+    Just previous -> (rsClientMap . at key)
+      ?= (previous & csStatus .~ Failed result)
+    Nothing -> pure ()
+
+markAsErrored
+  :: (MonadState (RaceState ht bt a) m)
+  => Async (CurlResponse_ ht bt)
+  -> SomeException
+  -> m ()
+markAsErrored key result = do
+  maybePrevious <- use $ rsClientMap . at key
+
+  case maybePrevious of
+    Just previous -> (rsClientMap . at key)
+      ?= (previous & csStatus .~ Errored result)
+    Nothing -> pure ()
+
+waitForOne
+  :: (Eq a, MonadIO m, MonadState (RaceState ht bt a) m)
+  => m (Maybe (Async (CurlResponse_ ht bt), a))
+waitForOne = do
+  debug <- use rsDebug
+  providerMap <- use rsClientMap
+
+  let asyncs = _csOptions <$> onlyPending providerMap
+
+  if null asyncs then pure Nothing else do
+    winner <- liftIO $ waitAnyCatch (M.keys asyncs)
+
+    case winner of
+      (as, Right a) -> do
+        handler <- use rsHandler
+        check <- use rsChecker
+        returnLast <- use rsReturnLast
+        result <- liftIO $ handler a
+
+        if check result then do
+          markAsSuccessful as result
+          cancelRemaining
+
+          when debug . liftIO $ do
+            TIO.putStr "[racer] Winner: "
+            print (asyncThreadId as)
+
+          pure $ Just (as, result)
+          else do
+            markAsFailure as result
+
+            remaining <- M.keys . onlyPending <$> use rsClientMap
+
+            if returnLast && null remaining
+              then do
+                when debug . liftIO $ do
+                  TIO.putStr "[racer] Reached last. Returning: "
+                  print (asyncThreadId as)
+
+                pure $ Just (as, result)
+              else waitForOne
+      (as, Left ex) -> markAsErrored as ex >> waitForOne
diff --git a/src/Network/Craze/Types.hs b/src/Network/Craze/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Craze/Types.hs
@@ -0,0 +1,110 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Network.Craze.Types where
+
+import Control.Exception (SomeException)
+
+import Data.ByteString    (ByteString)
+import Data.Default.Class (Default, def)
+import Data.Text          (Text)
+import Network.Curl       (CurlOption, CurlResponse_, respBody)
+
+-- | A 'RacerHandler' is simply a function for transforming a response after it
+-- is received. The handler is only applied to successful requests before they
+-- are checked by the 'RacerChecker'.
+--
+-- This is primarily for extracting or parsing a 'CurlResponse_' before doing
+-- any further work. The type returned by the handler will be used as the
+-- input of the checker and will be the return type of functions like
+-- 'raceGet'.
+--
+type RacerHandler headerTy bodyTy a = CurlResponse_ headerTy bodyTy -> IO a
+
+-- | A function that computes whether or not a result is valid or not.
+--
+-- A racer will discard successful responses it get from its clients if they do
+-- not pass the checker.
+--
+-- This step allows the racer to potentially discard responses that, while
+-- technically successful, do not contain the expected result (e.g. APIs that
+-- return errors as HTTP 200s, rate limitting messages, or unexpected formats).
+--
+type RacerChecker a = a -> Bool
+
+-- | A provider is simply a factory function for 'ProviderOptions', which are
+-- used to configure a client.
+type RacerProvider = IO ProviderOptions
+
+-- | Configuration used to set up an individual client in the race.
+data ProviderOptions = ProviderOptions
+  { -- | Options to pass down to Curl.
+    poOptions :: [CurlOption]
+    -- | Number of microseconds to delay the request by.
+    --
+    -- Delays can be used to give other clients a headstart. This is useful
+    -- in cases were some clients are more costly to use than others (e.g.
+    -- Bandwidth costs, resource usage, etc).
+    --
+  , poDelay   :: Maybe Int
+    -- | A tag to identify this type provider.
+    --
+    -- Tags are not required to be unique, but they are generally more helpful
+    -- if they are.
+  , poTag     :: Text
+  } deriving (Show)
+
+instance Default ProviderOptions where
+  def = ProviderOptions
+    { poOptions = []
+    , poDelay = Nothing
+    , poTag = "default"
+    }
+
+-- | The status of running a single client.
+data ClientStatus a
+  -- | A successful response (passed the checker). A race will usually only
+  -- have one successful response.
+  = Successful a
+  -- | A response that was received but failed to pass the checker.
+  | Failed a
+  -- | An exception thrown while using the client.
+  | Errored SomeException
+  -- | The operation is still pending, was cancelled, or was never started.
+  | Pending
+  deriving (Show)
+
+-- | The result of a racing operation. This can be used to collect statistics
+-- on which providers win more often, etc.
+data RacerResult a = RacerResult
+  { rrResponse  :: Maybe a
+  , rrWinner    :: Maybe ProviderOptions
+  , rrProviders :: [RacerProvider]
+  , rrStatuses  :: [(Text, ClientStatus a)]
+  }
+
+-- | A record describing the rules for racing requests.
+data Racer headerTy bodyTy a = Racer
+  { racerHandler    :: RacerHandler headerTy bodyTy a
+  , racerChecker    :: RacerChecker a
+  -- | On a `Racer`, each `RaceProvider` represents a separate client
+  -- configuration. When performing a race, each provider will be used to spwan
+  -- a client and perform a request. This allows one to control the number of
+  -- requests performed and with which `CurlOption`s.
+  , racerProviders  :: [RacerProvider]
+  -- | When set to `True`, debugging messages will be written to stdout.
+  , racerDebug      :: Bool
+  -- | When set to `True`, the Racer will attempt to return the last response
+  -- in the event that all responses failed to pass the checker. This can be
+  -- used for identifying error conditions.
+  , racerReturnLast :: Bool
+  }
+
+instance Default (Racer [(String,String)] ByteString ByteString) where
+  def = Racer
+    { racerHandler = pure . respBody
+    , racerChecker = const True
+    , racerProviders = []
+    , racerDebug = False
+    , racerReturnLast = False
+    }
