diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0.0
+
+- Initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Edward Yang
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,10 @@
+# captcha-haskell
+
+[![Hackage](http://img.shields.io/hackage/v/captcha-core.svg)](https://hackage.haskell.org/package/captcha-core)
+
+This repository is split into multiple packages:
+- [captcha-core](https://github.com/qwbarch/captcha-haskell/tree/main/core)
+- [captcha-2captcha](https://github.com/qwbarch/captcha-haskell/tree/main/2captcha)
+- [captcha-capmonster](https://github.com/qwbarch/captcha-haskell/tree/main/capmonster)
+
+Visit the [microsite](https://qwbarch.github.io/captcha-haskell/) for more information.
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/captcha-core.cabal b/captcha-core.cabal
new file mode 100644
--- /dev/null
+++ b/captcha-core.cabal
@@ -0,0 +1,62 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.34.4.
+--
+-- see: https://github.com/sol/hpack
+
+name:           captcha-core
+version:        0.1.0.0
+synopsis:       A package for integrating a variety of captcha solving services.
+description:    A package for integrating a variety of captcha solving services.
+                .
+                Feature list:
+                .
+                * Makes use of the [lens](https://hackage.haskell.org/package/lens) package
+                * Mtl-style typeclass, allowing you to use your own monad transformer stack
+                * No exceptions are thrown. Errors are shown explicitly through its type signature
+                * Minimal test suite provided for each captcha type
+                * Captcha services are separated into different packages. Use what you need!
+category:       Network
+homepage:       https://github.com/qwbarch/captcha-haskell#readme
+bug-reports:    https://github.com/qwbarch/captcha-haskell/issues
+author:         Edward Yang
+maintainer:     qwbarch@gmail.com
+copyright:      2021 Edward Yang
+license:        MIT
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://github.com/qwbarch/captcha-haskell
+
+library
+  exposed-modules:
+      Captcha
+      Captcha.Internal
+      Captcha.Internal.Monad
+      Captcha.Internal.Monad.Class
+      Captcha.Internal.Request
+      Captcha.Internal.Types
+  other-modules:
+      Paths_captcha_core
+  hs-source-dirs:
+      src
+  ghc-options: -Wall -Wno-name-shadowing
+  build-depends:
+      aeson >=1.5.6 && <1.6
+    , base >=4.7 && <5
+    , bytestring >=0.10.12.0 && <0.11
+    , cookie >=0.4.5 && <0.5
+    , data-default-extra >=0.0.1 && <0.1
+    , lens >=4.19.2 && <5.1
+    , mtl >=2.2.2 && <2.3
+    , o-clock >=1.2.1 && <1.3
+    , string-conversions >=0.4.0.1 && <0.5
+    , text >=1.2.4.1 && <1.3
+    , unliftio >=0.2.20 && <0.3
+    , wreq >=0.5.3.3 && <0.6
+  default-language: Haskell2010
diff --git a/src/Captcha.hs b/src/Captcha.hs
new file mode 100644
--- /dev/null
+++ b/src/Captcha.hs
@@ -0,0 +1,47 @@
+-- |
+-- Module: Captcha
+-- Copyright: (c) 2022 Edward Yang
+-- License: MIT
+--
+-- This module exports functions that follow the pvp versioning policies.
+module Captcha
+  ( module Captcha.Internal.Types,
+    module Captcha.Internal.Monad,
+    module Captcha.Internal.Monad.Class,
+  )
+where
+
+import Captcha.Internal.Monad
+import Captcha.Internal.Monad.Class
+import Captcha.Internal.Types
+  ( FunCaptcha,
+    HCaptcha,
+    HasAction (..),
+    HasAddress (..),
+    HasApiKey (..),
+    HasAuth (..),
+    HasBody (..),
+    HasCaptchaKey (..),
+    HasCaptchaUrl (..),
+    HasCookies (..),
+    HasDataS (..),
+    HasInvisible (..),
+    HasMinScore (..),
+    HasPassword (..),
+    HasPollingInterval (..),
+    HasPort (..),
+    HasProtocol (..),
+    HasProxy (..),
+    HasRqData (..),
+    HasServiceUrl (..),
+    HasTimeoutDuration (..),
+    HasUserAgent (..),
+    HasUsername (..),
+    ImageCaptcha,
+    Proxy (..),
+    ProxyAuth (..),
+    ProxyProtocol (..),
+    ReCaptchaV2,
+    ReCaptchaV3,
+    TextCaptcha,
+  )
diff --git a/src/Captcha/Internal.hs b/src/Captcha/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Captcha/Internal.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE FlexibleContexts #-}
+
+-- |
+-- Module: Captcha.Internal
+-- Copyright: (c) 2022 Edward Yang
+-- License: MIT
+--
+-- This module is for internal-use and does not follow pvp versioning policies.
+module Captcha.Internal where
+
+import Captcha.Internal.Types (HasAddress (address), HasAuth (auth), HasCookies (cookies), HasPassword (password), HasPort (port), HasProtocol (protocol), HasProxy (proxy), HasUsername (username), Proxy)
+import Control.Lens (preview, view, (^?), _Just)
+import Data.ByteString.Builder (toLazyByteString)
+import Data.String.Conversions (cs)
+import Data.Text (Text)
+import Data.Text.Lazy (toStrict)
+import Data.Text.Lazy.Encoding (decodeUtf8)
+import Web.Cookie (Cookies)
+import qualified Web.Cookie as Cookie
+
+-- | Render the cookies as a lazy text.
+renderCookies :: HasCookies a Cookies => a -> Text
+renderCookies = toStrict . decodeUtf8 . toLazyByteString . Cookie.renderCookies . view cookies
+
+-- | Retrieve the proxy's type as, converted into 'Text'.
+getProxyType :: HasProxy a (Maybe Proxy) => a -> Maybe Text
+getProxyType captcha = cs . show <$> captcha ^? proxy . _Just . protocol
+
+-- | Retrieve the proxy's host address.
+getProxyAddress :: HasProxy a (Maybe Proxy) => a -> Maybe Text
+getProxyAddress = preview $ proxy . _Just . address
+
+-- | Retrieve the proxy's port.
+getProxyPort :: HasProxy a (Maybe Proxy) => a -> Maybe Int
+getProxyPort = preview $ proxy . _Just . port
+
+-- | Retrieve the proxy's authentication username.
+getProxyUsername :: HasProxy a (Maybe Proxy) => a -> Maybe Text
+getProxyUsername = preview $ proxy . _Just . auth . _Just . username
+
+-- | Retrieve the proxy's authentication password.
+getProxyPassword :: HasProxy a (Maybe Proxy) => a -> Maybe Text
+getProxyPassword = preview $ proxy . _Just . auth . _Just . password
diff --git a/src/Captcha/Internal/Monad.hs b/src/Captcha/Internal/Monad.hs
new file mode 100644
--- /dev/null
+++ b/src/Captcha/Internal/Monad.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+-- |
+-- Module: Captcha.Internal.Monad
+-- Copyright: (c) 2022 Edward Yang
+-- License: MIT
+--
+-- This module is for internal-use and does not follow pvp versioning policies.
+module Captcha.Internal.Monad where
+
+import Control.Lens.TH (makeClassy)
+import Control.Monad.Cont (MonadIO (liftIO))
+import Control.Monad.Reader (MonadReader, ReaderT (ReaderT))
+import Network.Wreq.Session (Session, newSession)
+import UnliftIO (MonadUnliftIO)
+
+-- | Effect providing an environment required to solve captchas.
+newtype Captcha a = Captcha
+  { runCaptcha :: CaptchaEnv -> IO a
+  }
+  deriving
+    ( Functor,
+      Applicative,
+      Monad,
+      MonadIO,
+      MonadReader CaptchaEnv,
+      MonadUnliftIO
+    )
+    via ReaderT CaptchaEnv IO
+
+-- | Provides an HTTP 'Session' to be reused for each request.
+newtype CaptchaEnv = CaptchaEnv
+  { _session :: Session
+  }
+
+makeClassy ''CaptchaEnv
+
+-- | Create the environment required to solve captchas.
+mkCaptchaEnv :: MonadIO m => m CaptchaEnv
+mkCaptchaEnv = CaptchaEnv <$> liftIO newSession
diff --git a/src/Captcha/Internal/Monad/Class.hs b/src/Captcha/Internal/Monad/Class.hs
new file mode 100644
--- /dev/null
+++ b/src/Captcha/Internal/Monad/Class.hs
@@ -0,0 +1,85 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeFamilies #-}
+
+-- |
+-- Module: Captcha.Internal.Monad.Class
+-- Copyright: (c) 2022 Edward Yang
+-- License: MIT
+--
+-- This module is for internal-use and does not follow pvp versioning policies.
+module Captcha.Internal.Monad.Class where
+
+import Captcha.Internal.Types (HasApiKey, HasPollingInterval, HasTimeoutDuration)
+import Data.Aeson (Value)
+import Data.ByteString.Lazy (ByteString)
+import Data.Text (Text)
+import Network.Wreq (Response)
+import Time (Millisecond, Time)
+
+-- | Abstracts over a captcha solving service.
+class Monad m => MonadCaptcha api r m where
+  -- | An error specific to the captcha solving service.
+  type CaptchaError api r m
+
+  -- | Submit a task to be solved by the api service.
+  createTask ::
+    CaptchaRequest api ctx r m =>
+    -- | The captcha to be solved.
+    ctx ->
+    -- | Captcha id to be used with 'getTask'.
+    m (Either (CaptchaError api r m) (CaptchaId ctx))
+
+  -- | Attempt to retrieve the answer of the captcha.
+  getTask ::
+    CaptchaResponse api ctx =>
+    -- | The captcha service's API key.
+    Text ->
+    -- | The captcha to check the answer of.
+    CaptchaId ctx ->
+    -- | The captcha's solution.
+    m (Either (CaptchaError api r m) Text)
+
+  -- |
+  -- Solves a captcha by submitting it with 'createTask' and then polling with 'getTask'
+  -- until the answer is ready.
+  --
+  -- This will poll until the configured timeout duration is past.
+  -- Its default value depends on the captcha service.
+  solve ::
+    ( CaptchaRequest api ctx r m,
+      CaptchaResponse api ctx,
+      HasApiKey ctx Text,
+      HasPollingInterval ctx (Maybe (Time Millisecond)),
+      HasTimeoutDuration ctx (Maybe (Time Millisecond))
+    ) =>
+    -- | Captcha to be solved.
+    ctx ->
+    -- | The captcha's solution.
+    m (Either (CaptchaError api r m) Text)
+
+-- |
+-- Different captcha services have different request formats.
+-- This abstracts over it and sends the correct HTTP request.
+class CaptchaRequest api ctx r m where
+  -- | Send a request using the given captcha context.
+  request ::
+    -- | The captcha to be solved.
+    ctx ->
+    -- | The url to send the request to.
+    Text ->
+    m (Response ByteString)
+
+-- |
+-- Different captcha services have different response formats.
+-- This abstracts over it and provides the captcha result.
+class CaptchaResponse api ctx where
+  -- | Parse the captcha result from the given json.
+  parseResult :: Value -> Maybe Value
+
+-- | Identifier for retrieving a captcha's answer.
+newtype CaptchaId ctx = CaptchaId
+  { unCaptchaId :: Integer
+  }
+  deriving (Show, Eq, Ord)
diff --git a/src/Captcha/Internal/Request.hs b/src/Captcha/Internal/Request.hs
new file mode 100644
--- /dev/null
+++ b/src/Captcha/Internal/Request.hs
@@ -0,0 +1,27 @@
+-- |
+-- Module: Captcha.Internal.Request
+-- Copyright: (c) 2022 Edward Yang
+-- License: MIT
+--
+-- This module is for internal-use and does not follow pvp versioning policies.
+module Captcha.Internal.Request where
+
+import Captcha.Internal.Monad (HasCaptchaEnv (session))
+import Control.Lens ((^.))
+import Control.Monad.Cont (MonadIO (liftIO))
+import Control.Monad.Reader (MonadReader (ask))
+import Data.ByteString.Lazy (ByteString)
+import Data.Text (Text, unpack)
+import Network.Wreq (Options, Response)
+import qualified Network.Wreq.Session as Session
+import Network.Wreq.Types (Postable)
+
+-- | Send a POST request with the given session from 'CaptchaEnv'.
+post :: (HasCaptchaEnv r, MonadReader r m, MonadIO m, Postable a) => Options -> Text -> a -> m (Response ByteString)
+post options url payload =
+  ask >>= \env -> liftIO $ Session.postWith options (env ^. session) (unpack url) payload
+
+-- | Send a GET request with the given session from 'CaptchaEnv'.
+get :: (HasCaptchaEnv r, MonadReader r m, MonadIO m) => Options -> Text -> m (Response ByteString)
+get options url =
+  ask >>= \env -> liftIO $ Session.getWith options (env ^. session) (unpack url)
diff --git a/src/Captcha/Internal/Types.hs b/src/Captcha/Internal/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Captcha/Internal/Types.hs
@@ -0,0 +1,197 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DuplicateRecordFields #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE StrictData #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+-- |
+-- Module: Captcha.Internal.Types
+-- Copyright: (c) 2022 Edward Yang
+-- License: MIT
+--
+-- This module is for internal-use and does not follow pvp versioning policies.
+module Captcha.Internal.Types where
+
+import Control.Lens.TH (makeFieldsNoPrefix)
+import Data.Default (Default (def))
+import Data.Text (Text)
+import GHC.Generics (Generic)
+import Time (Millisecond, Time)
+import Web.Cookie (Cookies)
+
+-- | 'Default' instance for 'Bool' is not defined by default.
+instance Default Bool where
+  def = False
+
+-- | Proxy protocol.
+data ProxyProtocol = Http | Https | Socks4 | Socks5 deriving (Show)
+
+instance Default ProxyProtocol where
+  def = Http
+
+-- | Proxy authentication.
+data ProxyAuth = ProxyAuth
+  { _username :: Text,
+    _password :: Text
+  }
+  deriving (Generic, Default, Show)
+
+makeFieldsNoPrefix ''ProxyAuth
+
+-- | Proxy to be used when solving a captcha.
+data Proxy = Proxy
+  { -- | Proxy address.
+    _address :: Text,
+    -- | Protocol of the proxy.
+    _protocol :: ProxyProtocol,
+    -- | Proxy port.
+    _port :: Int,
+    -- | Proxy authentication, if required.
+    _auth :: Maybe ProxyAuth
+  }
+  deriving (Generic, Default, Show)
+
+makeFieldsNoPrefix ''Proxy
+
+-- | Parameters for solving a captcha with text within an image.
+data ImageCaptcha = ImageCaptcha
+  { -- | The captcha solver's API key.
+    _apiKey :: Text,
+    -- | The interval to poll for the captcha's answer.
+    _pollingInterval :: Maybe (Time Millisecond),
+    -- | The duration to keep polling for the answer.
+    _timeoutDuration :: Maybe (Time Millisecond),
+    -- | The image, encoded in base-64.
+    _body :: Text
+  }
+  deriving (Generic, Default, Show)
+
+makeFieldsNoPrefix ''ImageCaptcha
+
+-- | Parameters for solving a text captcha.
+data TextCaptcha = TextCaptcha
+  { -- | The captcha solver's API key.
+    _apiKey :: Text,
+    -- | The interval to poll for the captcha's answer.
+    _pollingInterval :: Maybe (Time Millisecond),
+    -- | The duration to keep polling for the answer.
+    _timeoutDuration :: Maybe (Time Millisecond),
+    -- | The text captcha to solve.
+    _body :: Text
+  }
+  deriving (Generic, Default, Show)
+
+makeFieldsNoPrefix ''TextCaptcha
+
+-- | Parameters for solving Arkose Lab's FunCaptcha.
+data FunCaptcha = FunCaptcha
+  { -- | The captcha solver's API key.
+    _apiKey :: Text,
+    -- | The interval to poll for the captcha's answer.
+    _pollingInterval :: Maybe (Time Millisecond),
+    -- | The duration to keep polling for the answer.
+    _timeoutDuration :: Maybe (Time Millisecond),
+    -- | Url where the captcha is found.
+    _captchaUrl :: Text,
+    -- | FunCaptcha's __data-pkey__ value.
+    _captchaKey :: Text,
+    -- | FunCaptcha's __surl__ service url value.
+    _serviceUrl :: Maybe Text,
+    -- | User agent to be used when solving the captcha.
+    _userAgent :: Maybe Text,
+    -- | Proxy to be used when solving the captcha.
+    _proxy :: Maybe Proxy,
+    -- | Cookies to be used when solving the captcha.
+    _cookies :: Cookies
+  }
+  deriving (Generic, Default, Show)
+
+makeFieldsNoPrefix ''FunCaptcha
+
+-- | Parameters for solving Google's reCAPTCHA v2.
+data ReCaptchaV2 = ReCaptchaV2
+  { -- | The captcha solver's API key.
+    _apiKey :: Text,
+    -- | The interval to poll for the captcha's answer.
+    _pollingInterval :: Maybe (Time Millisecond),
+    -- | The duration to keep polling for the answer.
+    _timeoutDuration :: Maybe (Time Millisecond),
+    -- | Url where the captcha is found.
+    _captchaUrl :: Text,
+    -- | reCAPTCHA v2's __data-sitekey__ value.
+    _captchaKey :: Text,
+    -- | reCAPTCHA's v2's __data-s__ value.
+    _dataS :: Maybe Text,
+    -- | Is the reCAPTCHA an __invisible__ or __normal__ captcha?
+    _invisible :: Bool,
+    -- | User agent to be used when solving the captcha.
+    _userAgent :: Maybe Text,
+    -- | Proxy to be used when solving the captcha.
+    _proxy :: Maybe Proxy,
+    -- | Cookies to be used when solving the captcha.
+    _cookies :: Cookies
+  }
+  deriving (Generic, Default, Show)
+
+makeFieldsNoPrefix ''ReCaptchaV2
+
+-- | Parameters for solving Google's reCAPTCHA v3.
+data ReCaptchaV3 = ReCaptchaV3
+  { -- | The captcha solver's API key.
+    _apiKey :: Text,
+    -- | The interval to poll for the captcha's answer.
+    _pollingInterval :: Maybe (Time Millisecond),
+    -- | The duration to keep polling for the answer.
+    _timeoutDuration :: Maybe (Time Millisecond),
+    -- | Url where the captcha is found.
+    _captchaUrl :: Text,
+    -- | reCAPTCHA v3's __sitekey__ value.
+    _captchaKey :: Text,
+    -- | reCAPTCHA v3's minimum score.
+    _minScore :: Double,
+    -- | reCAPTCHA v3's __action__ value.
+    _action :: Maybe Text,
+    -- | User agent to be used when solving the captcha.
+    _userAgent :: Maybe Text,
+    -- | Proxy to be used when solving the captcha.
+    _proxy :: Maybe Proxy,
+    -- | Cookies to be used when solving the captcha.
+    _cookies :: Cookies
+  }
+  deriving (Generic, Default, Show)
+
+makeFieldsNoPrefix ''ReCaptchaV3
+
+-- | Parameters for solving hCaptcha.
+data HCaptcha = HCaptcha
+  { -- | The captcha solver's API key.
+    _apiKey :: Text,
+    -- | The interval to poll for the captcha's answer.
+    _pollingInterval :: Maybe (Time Millisecond),
+    -- | The duration to keep polling for the answer.
+    _timeoutDuration :: Maybe (Time Millisecond),
+    -- | Url where the captcha is found.
+    _captchaUrl :: Text,
+    -- | hCaptcha's __data-sitekey__ value.
+    _captchaKey :: Text,
+    -- | Is the hCaptcha an __invisible__ or __normal__ captcha?
+    _invisible :: Bool,
+    -- |
+    -- Custom data used in some implementations of hCaptcha.
+    -- Note: You must provide a matching user agent if this is used.
+    _rqData :: Maybe Text,
+    -- | User agent to be used when solving the captcha. Required when using 'rqData'.
+    _userAgent :: Maybe Text,
+    -- | Proxy to be used when solving the captcha.
+    _proxy :: Maybe Proxy,
+    -- | Cookies to be used when solving the captcha.
+    _cookies :: Cookies
+  }
+  deriving (Show, Generic, Default)
+
+makeFieldsNoPrefix ''HCaptcha
