diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Version [0.1.0.0](https://github.com/blockfrost/blockfrost-haskell/compare/initial...0.1.0.0) (2021-09-14)
+
+* Initial release
+
+---
+
+`blockfrost-api` uses [PVP Versioning][1].
+
+[1]: https://pvp.haskell.org
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,13 @@
+Copyright 2021 blockfrost.io
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,44 @@
+# blockfrost-api
+
+Core types and Servant API definitions.
+
+## Exploring data types
+
+All data types have a `ToSample` instance
+for [servant-docs](https://hackage.haskell.org/package/servant-docs)
+which can be used to get a sample response in `cabal repl`:
+
+``` haskell
+λ: import Data.Proxy
+λ: import Servant.Docs
+λ: Just block = toSample (Proxy :: Proxy Block)
+λ: _blockHash block
+BlockHash "4ea1ba291e8eef538635a53e59fddba7810d1679631cc3aed7c8e6c4091a516a"
+```
+
+## Lenses
+
+Instead of using long record names, it is recommended
+to use provided lenses and a [lens](https://hackage.haskell.org/package/lens) or similar package.
+
+``` haskell
+λ: import Control.Lens (^.)
+λ: import Blockfrost.Lens
+λ: block ^. epoch
+Just (Epoch 425)
+```
+
+## Monetary values
+
+Ada values and values of assets are represented using [`Discrete`](https://hackage.haskell.org/package/safe-money/docs/Money.html#t:Discrete) type
+from [safe-money](https://hackage.haskell.org/package/safe-money) library.
+
+We use a type alias `type Lovelaces = Money.Discrete "ADA" "lovelace"`
+for Ada values and [`SomeDiscrete`](https://hackage.haskell.org/package/safe-money/docs/Money.html#t:SomeDiscrete) for asset values. This should allow working
+with monetary values safely, preventing summing different currencies.
+See the [blog post](https://ren.zone/articles/safe-money) by the library author.
+
+``` haskell
+λ: block ^. fees
+Just (Discrete "ADA" 1000000%1 592661)
+```
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/blockfrost-api.cabal b/blockfrost-api.cabal
new file mode 100644
--- /dev/null
+++ b/blockfrost-api.cabal
@@ -0,0 +1,167 @@
+cabal-version:       2.2
+name:                blockfrost-api
+version:             0.1.0.0
+synopsis:            API definitions for blockfrost.io
+description:         Core types and Servant API description
+homepage:            https://github.com/blockfrost/blockfrost-haskell
+license:             Apache-2.0
+license-file:        LICENSE
+author:              blockfrost.io
+maintainer:          srk@48.io
+copyright:           2021 blockfrost.io
+category:            Web
+build-type:          Simple
+
+extra-source-files:
+    CHANGELOG.md
+    LICENSE
+    README.md
+
+flag BuildFast
+     Default: True
+     Description: Turn off optimizations
+
+flag Production
+     Default: False
+     Manual: True
+     Description: Production build
+
+common libstuff
+  default-language:    Haskell2010
+  default-extensions:
+    DataKinds
+    DeriveAnyClass
+    DeriveGeneric
+    DerivingVia
+    GADTs
+    GeneralizedNewtypeDeriving
+    FlexibleContexts
+    FlexibleInstances
+    InstanceSigs
+    LambdaCase
+    MultiParamTypeClasses
+    QuasiQuotes
+    RankNTypes
+    ScopedTypeVariables
+    TemplateHaskell
+    TypeApplications
+    TypeFamilies
+    TypeOperators
+    ViewPatterns
+    OverloadedStrings
+  ghc-options:         -Wall -Wunused-packages -fno-warn-orphans
+  if flag(BuildFast)
+    ghc-options: -O0
+  if flag(Production)
+    ghc-options: -Werror
+
+library
+   import:              libstuff
+   hs-source-dirs:      src
+   exposed-modules:     Blockfrost.API
+                      , Blockfrost.API.Common
+                      , Blockfrost.API.Cardano
+                      , Blockfrost.API.Cardano.Accounts
+                      , Blockfrost.API.Cardano.Addresses
+                      , Blockfrost.API.Cardano.Assets
+                      , Blockfrost.API.Cardano.Blocks
+                      , Blockfrost.API.Cardano.Epochs
+                      , Blockfrost.API.Cardano.Ledger
+                      , Blockfrost.API.Cardano.Metadata
+                      , Blockfrost.API.Cardano.Network
+                      , Blockfrost.API.Cardano.Pools
+                      , Blockfrost.API.Cardano.Transactions
+                      , Blockfrost.API.IPFS
+                      , Blockfrost.API.NutLink
+                      , Blockfrost.Auth
+                      , Blockfrost.Env
+                      , Blockfrost.Lens
+                      , Blockfrost.Types
+                      , Blockfrost.Types.ApiError
+                      , Blockfrost.Types.Common
+                      , Blockfrost.Types.Cardano
+                      , Blockfrost.Types.Cardano.Accounts
+                      , Blockfrost.Types.Cardano.Addresses
+                      , Blockfrost.Types.Cardano.Assets
+                      , Blockfrost.Types.Cardano.Blocks
+                      , Blockfrost.Types.Cardano.Epochs
+                      , Blockfrost.Types.Cardano.Genesis
+                      , Blockfrost.Types.Cardano.Metadata
+                      , Blockfrost.Types.Cardano.Network
+                      , Blockfrost.Types.Cardano.Pools
+                      , Blockfrost.Types.Cardano.Transactions
+                      , Blockfrost.Types.IPFS
+                      , Blockfrost.Types.NutLink
+                      , Blockfrost.Types.Shared
+                      , Blockfrost.Types.Shared.Ada
+                      , Blockfrost.Types.Shared.Address
+                      , Blockfrost.Types.Shared.BlockHash
+                      , Blockfrost.Types.Shared.BlockIndex
+                      , Blockfrost.Types.Shared.Epoch
+                      , Blockfrost.Types.Shared.CBOR
+                      , Blockfrost.Types.Shared.Opts
+                      , Blockfrost.Types.Shared.POSIXMillis
+                      , Blockfrost.Types.Shared.Slot
+                      , Blockfrost.Types.Shared.TxHash
+                      , Blockfrost.Types.Shared.Amount
+                      , Blockfrost.Types.Shared.AssetId
+                      , Blockfrost.Types.Shared.PolicyId
+                      , Blockfrost.Types.Shared.PoolId
+                      , Blockfrost.Types.Shared.Quantity
+                      , Blockfrost.Util.LensRules
+                      , Blockfrost.Util.Pagination
+                      , Blockfrost.Util.Sorting
+                      , Blockfrost.Util.Tag
+   build-depends:       base >= 4.7 && < 5
+                      , bytestring
+                      , data-default-class
+                      , text
+                      , time
+                      , aeson
+                      , deriving-aeson
+                      , lens
+                      , template-haskell
+                      , servant                  ^>= 0.18
+                      , servant-docs
+                      , servant-multipart-api
+                      , safe-money
+                      , quickcheck-instances
+                      , QuickCheck
+
+test-suite blockfrost-api-tests
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Spec.hs
+  other-modules:       AmountSpec
+                     , APISpec
+                     , Cardano.Accounts
+                     , Cardano.Addresses
+                     , Cardano.Assets
+                     , Cardano.Epochs
+                     , Cardano.Ledger
+                     , Cardano.Metadata
+                     , Cardano.Network
+                     , Cardano.Pools
+                     , Cardano.Transactions
+                     , IPFS
+                     , NutLink
+  build-depends:       base >= 4.7 && < 5
+                     , blockfrost-api
+                     , aeson
+                     , bytestring
+                     , text
+                     , data-default
+                     , safe-money
+                     , raw-strings-qq
+                     , vector
+                     , hspec
+                     , tasty
+                     , tasty-hspec
+                     , tasty-hunit
+  build-tool-depends:
+    tasty-discover:tasty-discover
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/blockfrost/blockfrost-haskell
diff --git a/src/Blockfrost/API.hs b/src/Blockfrost/API.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API.hs
@@ -0,0 +1,136 @@
+-- | Blockfrost API definitions
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API
+  ( BlockfrostAPI (..)
+  , BlockfrostV0API (..)
+  , CommonAPI (..)
+  , CardanoAPI (..)
+  , IPFSAPI (..)
+  , NutLinkAPI (..)
+  , module Blockfrost.API.Cardano
+  , api
+  , api0
+  , ServantBlockfrostAPI
+  , BlockfrostAuth
+  , Project (..)
+  , Form (..)
+  ) where
+
+import Data.Proxy (Proxy (..))
+import Servant.API
+import Servant.API.Generic
+
+import Blockfrost.API.Cardano
+import Blockfrost.API.Common
+import Blockfrost.API.IPFS
+import Blockfrost.API.NutLink
+import Blockfrost.Auth
+import Blockfrost.Types
+import Blockfrost.Util.Tag (Tag)
+
+-- * API
+
+-- ** Our custom auth
+
+type BlockfrostAuth = ProjectAuth '[APIKeyInHeader "project_id"] Project
+
+-- ** Generic API types
+
+-- *** Toplevel
+
+newtype BlockfrostAPI route =
+  BlockfrostAPI
+    {
+      _apiV0 :: route
+      :- "api"
+      :> "v0"
+      :> BlockfrostAuth
+      :> ToServantApi BlockfrostV0API
+    } deriving (Generic)
+
+-- *** V0, requiring auth
+
+data BlockfrostV0API route =
+  BlockfrostV0API
+    {
+      _common  :: route :- ToServantApi CommonAPI
+    , _cardano :: route :- ToServantApi CardanoAPI
+    , _ipfs    :: route :- "ipfs" :> ToServantApi IPFSAPI
+    , _nutLink :: route :- "nutlink"  :> Tag "Nut.link" :> ToServantApi NutLinkAPI
+    } deriving (Generic)
+
+-- *** Services
+
+data CardanoAPI route =
+  CardanoAPI
+    {
+      _accounts
+      :: route
+      :- "accounts"
+      :> Tag "Cardano » Accounts"
+      :> ToServantApi AccountsAPI
+    , _addresses
+      :: route
+      :- "addresses"
+      :> Tag "Cardano » Addresses"
+      :> ToServantApi AddressesAPI
+    , _assets
+      :: route
+      :- "assets"
+      :> Tag "Cardano » Assets"
+      :> ToServantApi AssetsAPI
+    , _blocks
+      :: route
+      :- "blocks"
+      :> Tag "Cardano » Blocks"
+      :> ToServantApi BlocksAPI
+    , _epochs
+      :: route
+      :- "epochs"
+      :> Tag "Cardano » Epochs"
+      :> ToServantApi EpochsAPI
+    , _ledger
+      :: route
+      :- "genesis"
+      :> Tag "Cardano » Ledger"
+      :> ToServantApi LedgerAPI
+    , _metadata
+      :: route
+      :- "metadata"
+      :> Tag "Cardano » Metadata"
+      :> ToServantApi MetadataAPI
+    , _network
+      :: route
+      :- "network"
+      :> Tag "Cardano » Network"
+      :> ToServantApi NetworkAPI
+    , _pools
+      :: route
+      :- "pools"
+      :> Tag "Cardano » Pools"
+      :> ToServantApi PoolsAPI
+    , _transactions
+      :: route
+      :- "txs"
+      :> Tag "Cardano » Transactions"
+      :> ToServantApi TransactionsAPI
+    , _txSubmit
+      :: route
+      :- Summary "Submit a transaction"
+      :> Description "Submit an already serialized transaction to the network."
+      :> Tag "Cardano » Transactions"
+      :> "tx"
+      :> "submit"
+      :> ReqBody '[CBOR] CBORString
+      :> Post '[JSON] TxHash
+    } deriving (Generic)
+
+type ServantBlockfrostAPI = ToServantApi BlockfrostAPI
+
+api :: Proxy (ToServantApi BlockfrostAPI)
+api = genericApi (Proxy :: Proxy BlockfrostAPI)
+
+api0 :: Proxy (ToServantApi BlockfrostV0API)
+api0 = genericApi (Proxy :: Proxy BlockfrostV0API)
diff --git a/src/Blockfrost/API/Cardano.hs b/src/Blockfrost/API/Cardano.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano.hs
@@ -0,0 +1,27 @@
+-- | Cardano services
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano
+  ( module Blockfrost.API.Cardano.Accounts
+  , module Blockfrost.API.Cardano.Addresses
+  , module Blockfrost.API.Cardano.Assets
+  , module Blockfrost.API.Cardano.Blocks
+  , module Blockfrost.API.Cardano.Epochs
+  , module Blockfrost.API.Cardano.Ledger
+  , module Blockfrost.API.Cardano.Metadata
+  , module Blockfrost.API.Cardano.Network
+  , module Blockfrost.API.Cardano.Pools
+  , module Blockfrost.API.Cardano.Transactions
+  ) where
+
+import Blockfrost.API.Cardano.Accounts
+import Blockfrost.API.Cardano.Addresses
+import Blockfrost.API.Cardano.Assets
+import Blockfrost.API.Cardano.Blocks
+import Blockfrost.API.Cardano.Epochs
+import Blockfrost.API.Cardano.Ledger
+import Blockfrost.API.Cardano.Metadata
+import Blockfrost.API.Cardano.Network
+import Blockfrost.API.Cardano.Pools
+import Blockfrost.API.Cardano.Transactions
diff --git a/src/Blockfrost/API/Cardano/Accounts.hs b/src/Blockfrost/API/Cardano/Accounts.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano/Accounts.hs
@@ -0,0 +1,79 @@
+-- | Accounts API endpoints
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano.Accounts
+  where
+
+import Servant.API
+import Servant.API.Generic
+
+import Blockfrost.Types.Cardano.Accounts
+import Blockfrost.Types.Shared
+import Blockfrost.Util.Pagination
+import Blockfrost.Util.Sorting
+
+data AccountsAPI route =
+  AccountsAPI
+    {
+      _account
+        :: route
+        :- Summary "Specific account address"
+        :> Description "Obtain information about a specific stake account."
+        :> Capture "stake_address" Address
+        :> Get '[JSON] AccountInfo
+    , _accountRewards
+        :: route
+        :- Summary "Specific reward history"
+        :> Description "Obtain information about the reward history of a specific account."
+        :> Capture "stake_address" Address
+        :> "rewards"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AccountReward]
+     , _accountHistory
+        :: route
+        :- Summary "Account history"
+        :> Description "Obtain information about the history of a specific account."
+        :> Capture "stake_address" Address
+        :> "history"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AccountHistory]
+     , _accountDelegations
+        :: route
+        :- Summary "Account delegation history"
+        :> Description "Obtain information about the delegation of a specific account."
+        :> Capture "stake_address" Address
+        :> "delegations"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AccountDelegation]
+     , _accountRegistrations
+        :: route
+        :- Summary "Account registration history"
+        :> Description "Obtain information about the registrations and deregistrations of a specific account."
+        :> Capture "stake_address" Address
+        :> "registrations"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AccountRegistration]
+     , _accountWithdrawals
+        :: route
+        :- Summary "Account withdrawal history"
+        :> Description "Obtain information about the withdrawals of a specific account."
+        :> Capture "stake_address" Address
+        :> "withdrawals"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AccountWithdrawal]
+     , _accountMirs
+        :: route
+        :- Summary "Account MIR history"
+        :> Description "Obtain information about the MIRs of a specific account."
+        :> Capture "stake_address" Address
+        :> "mirs"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AccountMir]
+    } deriving (Generic)
diff --git a/src/Blockfrost/API/Cardano/Addresses.hs b/src/Blockfrost/API/Cardano/Addresses.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano/Addresses.hs
@@ -0,0 +1,52 @@
+-- | Addresses API endpoints
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano.Addresses
+  where
+
+import Servant.API
+import Servant.API.Generic
+
+import Blockfrost.Types.Cardano.Addresses
+import Blockfrost.Types.Shared
+import Blockfrost.Util.Pagination
+import Blockfrost.Util.Sorting
+
+data AddressesAPI route =
+  AddressesAPI
+    {
+      _addressInfo
+        :: route
+        :- Summary "Specific address"
+        :> Description "Obtain information about a specific address."
+        :> Capture "address" Address
+        :> Get '[JSON] AddressInfo
+    , _addressDetails
+        :: route
+        :- Summary "Address details"
+        :> Description "Obtain details about an address."
+        :> Capture "address" Address
+        :> "total"
+        :> Get '[JSON] AddressDetails
+    , _addressUtxos
+        :: route
+        :- Summary "Address UTXOs"
+        :> Description "UTXOs of the address."
+        :> Capture "address" Address
+        :> "utxos"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AddressUTXO]
+    , _addressTransactions
+        :: route
+        :- Summary "Address transactions"
+        :> Description "Transactions on the address."
+        :> Capture "address" Address
+        :> "transactions"
+        :> Pagination
+        :> Sorting
+        :> QueryParam "from" BlockIndex
+        :> QueryParam "to" BlockIndex
+        :> Get '[JSON] [AddressTransaction]
+    } deriving (Generic)
diff --git a/src/Blockfrost/API/Cardano/Assets.hs b/src/Blockfrost/API/Cardano/Assets.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano/Assets.hs
@@ -0,0 +1,68 @@
+-- | Assets API endpoints
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano.Assets
+  where
+
+import Servant.API
+import Servant.API.Generic
+
+import Blockfrost.Types.Cardano.Assets
+import Blockfrost.Types.Shared
+import Blockfrost.Util.Pagination
+import Blockfrost.Util.Sorting
+
+data AssetsAPI route =
+  AssetsAPI
+    {
+      _listAssets
+        :: route
+        :- Summary "Assets"
+        :> Description "List of assets."
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AssetInfo]
+    , _assetDetails
+        :: route
+        :- Summary "Specific asset"
+        :> Description "Information about a specific asset."
+        :> Capture "asset" AssetId
+        :> Get '[JSON] AssetDetails
+    , _assetHistory
+        :: route
+        :- Summary "Asset history"
+        :> Description "History of a specific asset."
+        :> Capture "asset" AssetId
+        :> "history"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AssetHistory]
+    , _assetTransactions
+        :: route
+        :- Summary "Asset transactions"
+        :> Description "List of a specific asset transactions"
+        :> Capture "asset" AssetId
+        :> "transactions"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AssetTransaction]
+    , _assetAddresses
+        :: route
+        :- Summary "Asset addresses"
+        :> Description "List of a addresses containing a specific asset"
+        :> Capture "asset" AssetId
+        :> "addresses"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AssetAddress]
+    , _listAssetsPolicy
+        :: route
+        :- Summary "Assets of a specific policy"
+        :> Description "List of asset minted under a specific policy."
+        :> "policy"
+        :> Capture "policy_id" PolicyId
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [AssetInfo]
+    } deriving (Generic)
diff --git a/src/Blockfrost/API/Cardano/Blocks.hs b/src/Blockfrost/API/Cardano/Blocks.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano/Blocks.hs
@@ -0,0 +1,82 @@
+-- | Blocks API endpoints
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano.Blocks
+  where
+
+import Servant.API
+import Servant.API.Generic
+
+import Blockfrost.Types.Cardano.Blocks
+import Blockfrost.Types.Shared
+import Blockfrost.Util.Pagination
+import Blockfrost.Util.Sorting
+
+data BlocksAPI route =
+  BlocksAPI
+    {
+      _latest
+        :: route
+        :- Summary "Latest block"
+        :> Description "Return the latest block available to the backends, \
+                        \also known as the tip of the blockchain."
+        :> "latest"
+        :> Get '[JSON] Block
+    , _latestTxs
+        :: route
+        :- Summary "Latest block transactions"
+        :> Description "Return the transactions within the latest block."
+        :> "latest"
+        :> "txs"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [TxHash]
+     , _block
+        :: route
+        :- Summary "Latest block transactions"
+        :> Description "Return the transactions within the latest block."
+        :> Capture "hash_or_number" (Either Integer BlockHash)
+        :> Get '[JSON] Block
+     , __blockSlot
+        :: route
+        :- Summary "Specific block in a slot"
+        :> Description "Return the content of a requested block for a specific slot."
+        :> "slot"
+        :> Capture "slot_number" Slot
+        :> Get '[JSON] Block
+     , __blockEpochSlot
+        :: route
+        :- Summary "Specific block in a slot in an epoch"
+        :> Description "Return the content of a requested block for a specific slot in an epoch."
+        :> "epoch"
+        :> Capture "epoch_number" Epoch
+        :> "slot"
+        :> Capture "slot_number" Slot
+        :> Get '[JSON] Block
+      , _blockNext
+        :: route
+        :- Summary "Listing of next blocks"
+        :> Description "Return the list of blocks following a specific block."
+        :> Capture "hash_or_number" (Either Integer BlockHash)
+        :> "next"
+        :> Pagination
+        :> Get '[JSON] [Block]
+      , _blockPrevious
+        :: route
+        :- Summary "Listing of preious blocks"
+        :> Description "Return the list of blocks preceeding a specific block."
+        :> Capture "hash_or_number" (Either Integer BlockHash)
+        :> "previous"
+        :> Pagination
+        :> Get '[JSON] [Block]
+      , _blockTxs
+        :: route
+        :- Summary "Block transactions"
+        :> Description "Return the transactions within the block."
+        :> Capture "hash_or_number" (Either Integer BlockHash)
+        :> "txs"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [TxHash]
+    } deriving (Generic)
diff --git a/src/Blockfrost/API/Cardano/Epochs.hs b/src/Blockfrost/API/Cardano/Epochs.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano/Epochs.hs
@@ -0,0 +1,97 @@
+-- | Epochs API endpoints
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano.Epochs
+  where
+
+import Servant.API
+import Servant.API.Generic
+
+import Blockfrost.Types.Cardano.Epochs
+import Blockfrost.Types.Shared
+import Blockfrost.Util.Pagination
+import Blockfrost.Util.Sorting
+
+data EpochsAPI route =
+  EpochsAPI
+    {
+      _latestEpoch
+        :: route
+        :- Summary "Latest epoch"
+        :> Description "Return the information about the latest, therefore current, epoch."
+        :> "latest"
+        :> Get '[JSON] EpochInfo
+    , _latestEpochProtocolParams
+        :: route
+        :- Summary "Latest epoch protocol parameters"
+        :> Description "Return the protocol parameters for the latest epoch."
+        :> "latest"
+        :> "parameters"
+        :> Get '[JSON] ProtocolParams
+    , _getEpoch
+        :: route
+        :- Summary "Specific epoch"
+        :> Description "Return the content of the requested epoch."
+        :> Capture "epoch_number" Epoch
+        :> Get '[JSON] EpochInfo
+    , _getNextEpochs
+        :: route
+        :- Summary "List of next epochs"
+        :> Description "Return the list of epochs following a specific epoch."
+        :> Capture "epoch_number" Epoch
+        :> "next"
+        :> Pagination
+        :> Get '[JSON] [EpochInfo]
+     , _getPreviousEpochs
+        :: route
+        :- Summary "List of previous epochs"
+        :> Description "Return the list of epochs preceding a specific epoch."
+        :> Capture "epoch_number" Epoch
+        :> "previous"
+        :> Pagination
+        :> Get '[JSON] [EpochInfo]
+     , _getEpochStake
+        :: route
+        :- Summary "Stake distribution"
+        :> Description "Return the active stake distribution for the specified epoch."
+        :> Capture "epoch_number" Epoch
+        :> "stakes"
+        :> Pagination
+        :> Get '[JSON] [StakeDistribution]
+     , _getEpochStakeByPool
+        :: route
+        :- Summary "Stake distribution by pool"
+        :> Description "Return the active stake distribution for the epoch specified by stake pool."
+        :> Capture "epoch_number" Epoch
+        :> "stakes"
+        :> Capture "pool_id" PoolId
+        :> Pagination
+        :> Get '[JSON] [StakeDistribution]
+     , _getEpochBlocks
+        :: route
+        :- Summary "Block distribution"
+        :> Description "Return the blocks minted for the epoch specified."
+        :> Capture "epoch_number" Epoch
+        :> "blocks"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [BlockHash]
+     , _getEpochBlocksByPool
+        :: route
+        :- Summary "Block distribution"
+        :> Description "Return the block minted for the epoch specified by stake pool."
+        :> Capture "epoch_number" Epoch
+        :> "blocks"
+        :> Capture "pool_id" PoolId
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [BlockHash]
+    , _getEpochProtocolParams
+        :: route
+        :- Summary "Protocol parameters"
+        :> Description "Return the protocol parameters for the specified epoch."
+        :> Capture "epoch_number" Epoch
+        :> "parameters"
+        :> Get '[JSON] ProtocolParams
+    } deriving (Generic)
diff --git a/src/Blockfrost/API/Cardano/Ledger.hs b/src/Blockfrost/API/Cardano/Ledger.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano/Ledger.hs
@@ -0,0 +1,21 @@
+-- | Cardano Ledger endpoints
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano.Ledger
+  where
+
+import Servant.API
+import Servant.API.Generic
+
+import Blockfrost.Types.Cardano.Genesis
+
+data LedgerAPI route =
+  LedgerAPI
+    {
+      _genesis
+        :: route
+        :- Summary "Blockchain genesis"
+        :> Description "Return the information about blockchain genesis."
+        :> Get '[JSON] Genesis
+    } deriving (Generic)
diff --git a/src/Blockfrost/API/Cardano/Metadata.hs b/src/Blockfrost/API/Cardano/Metadata.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano/Metadata.hs
@@ -0,0 +1,53 @@
+-- | Cardano Metadata endpoints
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano.Metadata
+  where
+
+import Data.Text (Text)
+import Servant.API
+import Servant.API.Generic
+import Servant.Docs (DocCapture (..), ToCapture (..))
+
+import Blockfrost.Types.Cardano.Metadata
+import Blockfrost.Util.Pagination
+import Blockfrost.Util.Sorting
+
+data MetadataAPI route =
+  MetadataAPI
+    {
+      _txMetadataLabels
+        :: route
+        :- Summary "Transaction metadata labels"
+        :> Description "List of all used transaction metadata labels."
+        :> "txs"
+        :> "labels"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [TxMeta]
+    , _txMetadataByLabelJSON
+        :: route
+        :- Summary "Transaction metadata content in JSON"
+        :> Description "Transaction metadata per label."
+        :> "txs"
+        :> "labels"
+        :> Capture "label" Text
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [TxMetaJSON]
+    , _txMetadataByLabelCBOR
+        :: route
+        :- Summary "Transaction metadata content in CBOR"
+        :> Description "Transaction metadata per label."
+        :> "txs"
+        :> "labels"
+        :> Capture "label" Text
+        :> "cbor"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [TxMetaCBOR]
+    } deriving (Generic)
+
+instance ToCapture (Capture "label" Text) where
+  toCapture _ = DocCapture "label" "Metadata label"
diff --git a/src/Blockfrost/API/Cardano/Network.hs b/src/Blockfrost/API/Cardano/Network.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano/Network.hs
@@ -0,0 +1,21 @@
+-- | Cardano Network endpoints
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano.Network
+  where
+
+import Servant.API
+import Servant.API.Generic
+
+import Blockfrost.Types.Cardano.Network
+
+data NetworkAPI route =
+  NetworkAPI
+    {
+      _networkInfo
+        :: route
+        :- Summary "Network information"
+        :> Description "Return detailed network information."
+        :> Get '[JSON] Network
+    } deriving (Generic)
diff --git a/src/Blockfrost/API/Cardano/Pools.hs b/src/Blockfrost/API/Cardano/Pools.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano/Pools.hs
@@ -0,0 +1,98 @@
+-- | Cardano Pools endpoints
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano.Pools
+  where
+
+import Servant.API
+import Servant.API.Generic
+
+import Blockfrost.Types.Cardano.Pools
+import Blockfrost.Types.Shared
+import Blockfrost.Util.Pagination
+import Blockfrost.Util.Sorting
+
+data PoolsAPI route =
+  PoolsAPI
+    {
+      _listPools
+        :: route
+        :- Summary "List of stake pools"
+        :> Description "List of registered stake pools."
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [PoolId]
+    , _listRetiredPools
+        :: route
+        :- Summary "List of retired stake pools"
+        :> Description "List of already retired stake pools."
+        :> "retired"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [PoolEpoch]
+    , _listRetiringPools
+        :: route
+        :- Summary "List of retiring stake pools"
+        :> Description "List of stake pools retiring in the upcoming epochs"
+        :> "retiring"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [PoolEpoch]
+    , _getPool
+        :: route
+        :- Summary "Specific stake pool"
+        :> Description "Pool information."
+        :> Capture "pool_id" PoolId
+        :> Get '[JSON] PoolInfo
+   , _getPoolHistory
+        :: route
+        :- Summary "Stake pool history"
+        :> Description "History of stake pool parameters over epochs."
+        :> Capture "pool_id" PoolId
+        :> "history"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [PoolHistory]
+    , _getPoolMetadata
+        :: route
+        :- Summary "Stake pool metadata"
+        :> Description "Stake pool registration metadata."
+        :> Capture "pool_id" PoolId
+        :> "metadata"
+        :> Get '[JSON] (Maybe PoolMetadata)
+    , _getPoolRelays
+        :: route
+        :- Summary "Stake pool relays"
+        :> Description "Relays of a stake pool."
+        :> Capture "pool_id" PoolId
+        :> "relays"
+        :> Get '[JSON] [PoolRelay]
+    , _getPoolDelegators
+        :: route
+        :- Summary "Stake pool delegators"
+        :> Description "List of current stake pools delegators."
+        :> Capture "pool_id" PoolId
+        :> "delegators"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [PoolDelegator]
+    , _getPoolBlocks
+        :: route
+        :- Summary "Stake pool blocks"
+        :> Description "List of stake pool blocks."
+        :> Capture "pool_id" PoolId
+        :> "blocks"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [BlockHash]
+    , _getPoolUpdates
+        :: route
+        :- Summary "Stake pool updates"
+        :> Description "List of certificate updates to the stake pool."
+        :> Capture "pool_id" PoolId
+        :> "updates"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [PoolUpdate]
+    } deriving (Generic)
diff --git a/src/Blockfrost/API/Cardano/Transactions.hs b/src/Blockfrost/API/Cardano/Transactions.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Cardano/Transactions.hs
@@ -0,0 +1,87 @@
+-- | Cardano Pools endpoints
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Cardano.Transactions
+  where
+
+import Servant.API
+import Servant.API.Generic
+
+import Blockfrost.Types.Cardano.Transactions
+import Blockfrost.Types.Shared
+
+data TransactionsAPI route =
+  TransactionsAPI
+    {
+      _tx
+        :: route
+        :- Summary "Specific transaction"
+        :> Description "Return content of the requested transaction."
+        :> Capture "hash" TxHash
+        :> Get '[JSON] Transaction
+    , _txUtxos
+        :: route
+        :- Summary "Transaction UTXOs"
+        :> Description "Return the inputs and UTXOs of the specific transaction."
+        :> Capture "hash" TxHash
+        :> "utxos"
+        :> Get '[JSON] TransactionUtxos
+    , _txStakes
+        :: route
+        :- Summary "Transaction stake addresses certificates "
+        :> Description "Obtain information about (de)registration of stake addresses within a transaction."
+        :> Capture "hash" TxHash
+        :> "stakes"
+        :> Get '[JSON] [TransactionStake]
+    , _txDelegations
+        :: route
+        :- Summary "Transaction delegation certificates"
+        :> Description "Obtain information about delegation certificates of a specific transaction."
+        :> Capture "hash" TxHash
+        :> "delegations"
+        :> Get '[JSON] [TransactionDelegation]
+    , _txWithdrawals
+        :: route
+        :- Summary "Transaction withdrawal"
+        :> Description "Obtain information about withdrawals of a specific transaction."
+        :> Capture "hash" TxHash
+        :> "withdrawals"
+        :> Get '[JSON] [TransactionWithdrawal]
+    , _txMirs
+        :: route
+        :- Summary "Transaction MIRs"
+        :> Description "Obtain information about Move Instantaneous Rewards (MIRs) of a specific transaction."
+        :> Capture "hash" TxHash
+        :> "mirs"
+        :> Get '[JSON] [TransactionMir]
+    , _txPoolUpdates
+        :: route
+        :- Summary "Transaction stake pool registration and update certificates"
+        :> Description "Obtain information about stake pool registration and update certificates of a specific transaction."
+        :> Capture "hash" TxHash
+        :> "pool_updates"
+        :> Get '[JSON] [TransactionPoolUpdate]
+    , _txPoolRetiring
+        :: route
+        :- Summary "Transaction stake pool retirement certificates"
+        :> Description "Obtain information about stake pool retirements within a specific transaction."
+        :> Capture "hash" TxHash
+        :> "pool_retires"
+        :> Get '[JSON] [TransactionPoolRetiring]
+    , _txMetadataJSON
+        :: route
+        :- Summary "Transaction metadata"
+        :> Description "Obtain the transaction metadata."
+        :> Capture "hash" TxHash
+        :> "metadata"
+        :> Get '[JSON] [TransactionMetaJSON]
+    , _txMetadataCBOR
+        :: route
+        :- Summary "Transaction metadata in CBOR"
+        :> Description "Obtain the transaction metadata in CBOR."
+        :> Capture "hash" TxHash
+        :> "metadata"
+        :> "cbor"
+        :> Get '[JSON] [TransactionMetaCBOR]
+    } deriving (Generic)
diff --git a/src/Blockfrost/API/Common.hs b/src/Blockfrost/API/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/Common.hs
@@ -0,0 +1,56 @@
+-- | Common services
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.Common
+  where
+
+import Blockfrost.Types
+import Blockfrost.Util.Tag (Tag)
+import Data.Text (Text)
+import Servant.API
+import Servant.API.Generic
+
+data CommonAPI route =
+  CommonAPI
+    {
+      _getRoot
+        :: route
+        :- Summary "Root endpoint"
+        :> Description "Root endpoint has no other function than to point end users to documentation."
+        :> Tag "Health"
+        :> Get '[JSON] URLVersion
+    , _getHealth
+        :: route
+        :- Summary "Backend health status"
+        :> Description "Return backend status as a boolean. \
+                        \Your application should handle situations when backend for the given chain is unavailable."
+        :> Tag "Health"
+        :> "health"
+        :> Get '[JSON] Healthy
+    , _getClock
+        :: route
+        :- Summary "Current backend time"
+        :> Description "This endpoint provides the current UNIX time. \
+                         \Your application might use this to verify if \
+                         \the client clock is not out of sync."
+        :> Tag "Health"
+        :> "health"
+        :> "clock"
+        :> Get '[JSON] ServerTime
+    , _metrics
+        :: route
+        :- Summary "Blockfrost usage metrics"
+        :> Description "History of your Blockfrost usage metrics in the past 30 days."
+        :> Tag "Metrics"
+        :> "metrics"
+        :> Get '[JSON] [Metric]
+    , _metricsEndpoints
+        :: route
+        :- Summary "Blockfrost endpoint usage metrics"
+        :> Description "History of your Blockfrost usage metrics per endpoint in the past 30 days."
+        :> Tag "Metrics"
+        :> "metrics"
+        :> "endpoints"
+        :> Get '[JSON] [(Text, Metric)]
+    } deriving (Generic)
diff --git a/src/Blockfrost/API/IPFS.hs b/src/Blockfrost/API/IPFS.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/IPFS.hs
@@ -0,0 +1,84 @@
+-- | IPFS services
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.IPFS
+  where
+
+import Blockfrost.Types
+import Blockfrost.Util.Pagination
+import Blockfrost.Util.Sorting
+import Blockfrost.Util.Tag (Tag)
+import Data.Text (Text)
+import Servant.API
+import Servant.API.Generic
+import Servant.Docs (DocCapture (..), ToCapture (..))
+import Servant.Multipart.API
+
+data IPFSAPI route =
+  IPFSAPI
+    {
+      _add
+        :: route
+        :- Summary "Add a file or directory to IPFS"
+        :> Description "You need to `/ipfs/pin/add` an object to avoid it being garbage collected. \
+                        \This usage is being counted in your user account quota."
+        :> Tag "IPFS » Add"
+        :> "add"
+        :> MultipartForm Tmp Form
+        :> Post '[JSON] IPFSAdd
+    , _gateway
+        :: route
+        :- Summary "Relay to an IPFS gateway"
+        :> Description "Retrieve an object from the IFPS gateway. \
+                        \(Useful if you do not want to rely on a public gateway, such as ``ipfs.blockfrost.dev`)."
+        :> Tag "IPFS » Gateway"
+        :> "gateway"
+        :> Capture "IPFS_path" Text
+        :> Get '[PlainText, OctetStream] IPFSData
+    , _pin
+        :: route
+        :- Summary "Pin an object"
+        :> Description "Pinned objects are counted in your user storage quota."
+        :> Tag "IPFS » Pins"
+        :> "pin"
+        :> "add"
+        :> Capture "IPFS_path" Text
+        :> Post '[JSON] IPFSPinChange
+    , _listPins
+        :: route
+        :- Summary "List pinned objects"
+        :> Description "List objects pinned to local storage."
+        :> Tag "IPFS » Pins"
+        :> "pin"
+        :> "list"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [IPFSPin]
+    , _getPin
+        :: route
+        :- Summary "Get pinned object details"
+        :> Description "Obtain inormation about specific pinned object."
+        :> Tag "IPFS » Pins"
+        :> "pin"
+        :> "list"
+        :> Capture "IPFS_path" Text
+        :> Get '[JSON] IPFSPin
+    , _removePin
+        :: route
+        :- Summary "Remove pinned object from local storage"
+        :> Description "Remove pinned object from local storage"
+        :> Tag "IPFS » Pins"
+        :> "pin"
+        :> "remove"
+        :> Capture "IPFS_path" Text
+        :> Post '[JSON] IPFSPinChange
+    } deriving (Generic)
+
+instance ToCapture (Capture "IPFS_path" Text) where
+  toCapture _ = DocCapture "IPFS_path" "Path to the IPFS object"
+
+data Form = Form {
+    formFileName :: Text
+  , formFilePath :: FilePath
+  }
diff --git a/src/Blockfrost/API/NutLink.hs b/src/Blockfrost/API/NutLink.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/API/NutLink.hs
@@ -0,0 +1,56 @@
+-- | Nut.link services
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.API.NutLink
+  where
+
+import Blockfrost.Types
+import Blockfrost.Util.Pagination
+import Blockfrost.Util.Sorting
+import Data.Text (Text)
+import Servant.API
+import Servant.API.Generic
+import Servant.Docs (DocCapture (..), ToCapture (..))
+
+data NutLinkAPI route =
+  NutLinkAPI
+    {
+      _address
+        :: route
+        :- Summary "List metadata about specific address"
+        :> Description "List metadata about specific address"
+        :> Capture "address" Address
+        :> Get '[JSON] NutlinkAddress
+    , _listAddressTickers
+        :: route
+        :- Summary "List tickers for a specific metadata oracle"
+        :> Description "List tickers for a specific metadata oracle"
+        :> Capture "address" Address
+        :> "tickers"
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [NutlinkAddressTicker]
+    , _addressTickers
+        :: route
+        :- Summary "List of records of a specific ticker"
+        :> Description "List of records of a specific ticker"
+        :> Capture "address" Address
+        :> "tickers"
+        :> Capture "ticker" Text
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [NutlinkTicker]
+    , _tickers
+        :: route
+        :- Summary "List of records of a specific ticker"
+        :> Description "List of records of a specific ticker"
+        :> "tickers"
+        :> Capture "ticker" Text
+        :> Pagination
+        :> Sorting
+        :> Get '[JSON] [(Address, NutlinkTicker)]
+    } deriving (Generic)
+
+instance ToCapture (Capture "ticker" Text) where
+  toCapture _ = DocCapture "ticker" "Ticker for the pool record"
diff --git a/src/Blockfrost/Auth.hs b/src/Blockfrost/Auth.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Auth.hs
@@ -0,0 +1,74 @@
+-- | Blockfrost authentication schemes
+{-# LANGUAGE PolyKinds #-}
+
+module Blockfrost.Auth
+  ( APIKeyInHeader
+  , APIKeyInHeaderSettings (..)
+  , Env (..)
+  , Project (..)
+  , ProjectAuth
+  , mkProject
+  , mkProjectEnv
+  ) where
+
+import Data.Kind (Type)
+import Data.Proxy (Proxy (..))
+import Data.Text as Text
+import GHC.Generics (Generic)
+import GHC.TypeLits
+import Servant.API (HasLink (..), (:>))
+
+import qualified Data.Text
+
+import Data.String (IsString (..))
+
+import Blockfrost.Env
+
+-- * Authentication result and clients token
+
+data Project = Project {
+    projectEnv :: Env
+  , projectId  :: Text
+  }
+  deriving (Eq, Show, Generic)
+
+-- | Parse @Project@ from @Text@
+mkProject' :: Text -> Either Text Project
+mkProject' t =
+  let st = Data.Text.strip t
+      tEnv = Data.Text.dropEnd 32 st
+      token = Data.Text.drop (Data.Text.length tEnv) st
+  in Project <$> parseEnv tEnv <*> pure token
+
+-- | Parse @Project@ from @Text@ or fail with error
+mkProject :: Text -> Project
+mkProject = either (error . Data.Text.unpack) id . mkProject'
+
+-- | @Project@ constructor
+mkProjectEnv :: Env -> Text -> Project
+mkProjectEnv = Project
+
+instance IsString Project where
+  fromString = mkProject . Data.Text.pack
+
+-- * Common
+
+-- | The type of Auth scheme.
+data APIKeyInHeader (headerName :: Symbol)
+
+-- | Auth scheme settings
+-- Needs IO action to verify passed in token and maybe return Project
+newtype APIKeyInHeaderSettings =
+  APIKeyInHeaderSettings
+    { apiKeySettingsQueryProject :: Text -> IO (Maybe Project)
+    }
+
+-- * Custom Auth
+-- we use custom ProjectAuth instead of Auth from servant-auth
+-- to get rid of superfluous Contexts like JWTSettings and CookiesSettings
+
+data ProjectAuth (auths :: [Type]) val
+
+instance HasLink sub => HasLink (ProjectAuth (tag :: [Type]) value :> sub) where
+  type MkLink (ProjectAuth (tag :: [Type]) value :> sub) r = MkLink sub r
+  toLink toA _ = toLink toA (Proxy @sub)
diff --git a/src/Blockfrost/Env.hs b/src/Blockfrost/Env.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Env.hs
@@ -0,0 +1,32 @@
+-- | Blockfrost environments
+
+module Blockfrost.Env
+  ( Env (..)
+  , parseEnv
+  ) where
+
+import Data.Text (Text)
+import qualified Data.Text
+import GHC.Generics (Generic)
+import qualified Text.Read
+
+-- | Blockfrost environments
+--
+-- Corresponds to Network when creating a Blockfrost project.
+-- Each environment has separate token.
+data Env =
+    Alonzo
+  | Ipfs
+  | Mainnet
+  | Testnet
+  | Localhost
+  deriving (Eq, Read, Show, Ord, Generic)
+
+-- | Try parsing @Env@ from @Text@
+parseEnv :: Text -> Either Text Env
+parseEnv tEnv = case Text.Read.readMaybe (Data.Text.unpack $ Data.Text.toTitle tEnv) of
+  Just env -> pure env
+  Nothing ->
+    Left
+      $ "Unknown environment: `" <> tEnv <> "`"
+      <> " expecting one of `alonzo`, `ipfs`, `mainnet`, `testnet`, `localhost`"
diff --git a/src/Blockfrost/Lens.hs b/src/Blockfrost/Lens.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Lens.hs
@@ -0,0 +1,86 @@
+-- | Lenses for Blockfrost types
+
+{-# LANGUAGE FunctionalDependencies #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Blockfrost.Lens
+  where
+
+import Blockfrost.Types
+import Blockfrost.Util.LensRules
+import Control.Lens
+
+makeLensesWith blockfrostFieldRules ''URLVersion
+
+makeFields ''AccountInfo
+makeFields ''AccountReward
+makeFields ''AccountHistory
+makeFields ''AccountDelegation
+makeFields ''AccountRegistration
+makeFields ''AccountWithdrawal
+makeFields ''AccountMir
+
+makeLensesWith blockfrostFieldRules ''AddressInfo
+makeFields ''AddressDetails
+makeFields ''AddressUTXO
+makeFields ''AddressTransaction
+
+makeFields ''AssetInfo
+makeFields ''AssetDetails
+makeFields ''AssetOnChainMetadata
+makeFields ''AssetMetadata
+makeFields ''AssetHistory
+makeFields ''AssetTransaction
+makeFields ''AssetAddress
+
+makeFields ''Block
+
+makeFields ''EpochInfo
+makeFields ''ProtocolParams
+makeFields ''StakeDistribution
+makeFields ''PoolStakeDistribution
+
+makeFields ''Genesis
+
+makeFields ''TxMeta
+makeFields ''TxMetaJSON
+makeFields ''TxMetaCBOR
+
+makeFields ''Network
+makeFields ''NetworkSupply
+makeFields ''NetworkStake
+
+makeFields ''PoolEpoch
+makeFields ''PoolInfo
+makeFields ''PoolHistory
+makeFields ''PoolMetadata
+makeFields ''PoolRelay
+makeFields ''PoolDelegator
+makeFields ''PoolUpdate
+makeFields ''PoolRegistrationAction
+
+makeFields ''Transaction
+makeFields ''TransactionUtxos
+makeFields ''UtxoInput
+makeFields ''UtxoOutput
+makeFields ''TransactionStake
+makeFields ''TransactionDelegation
+makeFields ''TransactionWithdrawal
+makeFields ''TransactionMir
+makeFields ''TransactionPoolUpdate
+makeFields ''PoolUpdateMetadata
+makeFields ''TransactionPoolRetiring
+makeFields ''TransactionMetaJSON
+makeFields ''TransactionMetaCBOR
+
+makeLensesWith blockfrostFieldRules ''IPFSAdd
+makeLensesWith blockfrostFieldRules ''IPFSPinChange
+makeLensesWith blockfrostFieldRules ''IPFSPin
+
+makeFields ''NutlinkAddress
+makeFields ''NutlinkAddressTicker
+makeFields ''NutlinkTicker
+
+-- * Shared
+makeFields ''BlockIndex
+makePrisms ''Amount
diff --git a/src/Blockfrost/Types.hs b/src/Blockfrost/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types.hs
@@ -0,0 +1,17 @@
+-- | Blockfrost types
+
+module Blockfrost.Types
+  ( module Blockfrost.Types.ApiError
+  , module Blockfrost.Types.Cardano
+  , module Blockfrost.Types.Common
+  , module Blockfrost.Types.IPFS
+  , module Blockfrost.Types.NutLink
+  , module Blockfrost.Types.Shared
+  ) where
+
+import Blockfrost.Types.ApiError
+import Blockfrost.Types.Cardano
+import Blockfrost.Types.Common
+import Blockfrost.Types.IPFS
+import Blockfrost.Types.NutLink
+import Blockfrost.Types.Shared
diff --git a/src/Blockfrost/Types/ApiError.hs b/src/Blockfrost/Types/ApiError.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/ApiError.hs
@@ -0,0 +1,35 @@
+-- | Internal API error type
+
+{-# LANGUAGE RecordWildCards #-}
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.Types.ApiError
+  ( ApiError (..)
+  ) where
+
+import Data.Aeson
+import Data.Text (Text)
+import GHC.Generics
+
+-- | Fancy JSON error returned
+-- by the server
+data ApiError = ApiError
+  { apiError        :: Text
+  , apiErrorMessage :: Text
+  , apiErrorCode    :: Int
+  } deriving (Eq, Show, Generic)
+
+instance ToJSON ApiError  where
+  toJSON ApiError{..} =
+    object [ "error" .= toJSON apiError
+           , "message" .= toJSON apiErrorMessage
+           , "status_code" .= toJSON apiErrorCode
+           ]
+
+instance FromJSON ApiError  where
+  parseJSON = withObject "error" $ \o -> do
+    apiError <- o .: "error"
+    apiErrorMessage <- o .: "message"
+    apiErrorCode <- o .: "status_code"
+    pure $ ApiError {..}
diff --git a/src/Blockfrost/Types/Cardano.hs b/src/Blockfrost/Types/Cardano.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano.hs
@@ -0,0 +1,25 @@
+-- | Types of Cardano
+
+module Blockfrost.Types.Cardano
+  ( module Blockfrost.Types.Cardano.Accounts
+  , module Blockfrost.Types.Cardano.Addresses
+  , module Blockfrost.Types.Cardano.Assets
+  , module Blockfrost.Types.Cardano.Blocks
+  , module Blockfrost.Types.Cardano.Epochs
+  , module Blockfrost.Types.Cardano.Genesis
+  , module Blockfrost.Types.Cardano.Metadata
+  , module Blockfrost.Types.Cardano.Network
+  , module Blockfrost.Types.Cardano.Pools
+  , module Blockfrost.Types.Cardano.Transactions
+  ) where
+
+import Blockfrost.Types.Cardano.Accounts
+import Blockfrost.Types.Cardano.Addresses
+import Blockfrost.Types.Cardano.Assets
+import Blockfrost.Types.Cardano.Blocks
+import Blockfrost.Types.Cardano.Epochs
+import Blockfrost.Types.Cardano.Genesis
+import Blockfrost.Types.Cardano.Metadata
+import Blockfrost.Types.Cardano.Network
+import Blockfrost.Types.Cardano.Pools
+import Blockfrost.Types.Cardano.Transactions
diff --git a/src/Blockfrost/Types/Cardano/Accounts.hs b/src/Blockfrost/Types/Cardano/Accounts.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano/Accounts.hs
@@ -0,0 +1,204 @@
+-- | Responses for Cardano accounts queries
+
+module Blockfrost.Types.Cardano.Accounts
+  ( AccountInfo (..)
+  , AccountReward (..)
+  , AccountHistory (..)
+  , AccountDelegation (..)
+  , AccountRegistration (..)
+  , AccountRegistrationAction (..)
+  , AccountWithdrawal (..)
+  , AccountMir (..)
+  ) where
+
+import Blockfrost.Types.Shared
+import Deriving.Aeson
+import Servant.Docs (ToSample (..), samples, singleSample)
+
+-- | Information about an account, identified by its stake address
+data AccountInfo = AccountInfo
+  { _accountInfoStakeAddress       :: Address -- ^ Bech32 stake address
+  , _accountInfoActive             :: Bool -- ^ Registration state of an account
+  , _accountInfoActiveEpoch        :: Integer -- ^ Epoch of the most recent action - registration or deregistration
+  , _accountInfoControlledAmount   :: Lovelaces  -- ^ Balance of the account in Lovelaces
+  , _accountInfoRewardsSum         :: Lovelaces -- ^ Sum of all funds rewards for the account in the Lovelaces
+  , _accountInfoWithdrawalsSum     :: Lovelaces -- ^ Sum of all the withdrawals for the account in the Lovelaces
+  , _accountInfoReservesSum        :: Lovelaces -- ^ Sum of all funds from reserves for the account in the Lovelaces
+  , _accountInfoTreasurySum        :: Lovelaces -- ^ Sum of all funds from treasury for the account in the Lovelaces
+  , _accountInfoWithdrawableAmount :: Lovelaces -- ^ Sum of available rewards that haven't been withdrawn yet for the account in the Lovelaces
+  , _accountInfoPoolId             :: Maybe PoolId -- ^ Bech32 pool ID that owns the account
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_accountInfo", CamelToSnake]] AccountInfo
+
+instance ToSample AccountInfo where
+  toSamples = pure $ singleSample $ AccountInfo
+    { _accountInfoStakeAddress = "stake1ux3g2c9dx2nhhehyrezyxpkstartcqmu9hk63qgfkccw5rqttygt7"
+    , _accountInfoActive = True
+    , _accountInfoActiveEpoch = 412
+    , _accountInfoControlledAmount = 619154618165
+    , _accountInfoRewardsSum = 319154618165
+    , _accountInfoWithdrawalsSum = 12125369253
+    , _accountInfoReservesSum = 319154618165
+    , _accountInfoTreasurySum = 12000000
+    , _accountInfoWithdrawableAmount = 319154618165
+    , _accountInfoPoolId = pure "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    }
+
+-- | Reward received by an account
+data AccountReward = AccountReward
+  { _accountRewardEpoch  :: Epoch -- ^ Epoch of the associated reward
+  , _accountRewardAmount :: Lovelaces -- ^ Rewards for given epoch in Lovelaces
+  , _accountRewardPoolId :: PoolId -- ^ Bech32 pool ID being delegated to
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_accountReward", CamelToSnake]] AccountReward
+
+instance ToSample AccountReward where
+  toSamples = pure $ samples
+    [ AccountReward
+        { _accountRewardEpoch = 214
+        , _accountRewardAmount = 1395265
+        , _accountRewardPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+        }
+    , AccountReward
+        { _accountRewardEpoch = 215
+        , _accountRewardAmount = 58632
+        , _accountRewardPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+        }
+    , AccountReward
+        { _accountRewardEpoch = 216
+        , _accountRewardAmount = 0
+        , _accountRewardPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+        }
+    , AccountReward
+        { _accountRewardEpoch = 217
+        , _accountRewardAmount = 1395265
+        , _accountRewardPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+        }
+    ]
+
+-- | History of accounts stake delegation
+data AccountHistory = AccountHistory
+  { _accountHistoryActiveEpoch :: Integer -- ^ Epoch in which the stake was active
+  , _accountHistoryAmount      :: Lovelaces -- ^ Stake amount in Lovelaces
+  , _accountHistoryPoolId      :: PoolId -- ^ Bech32 ID of pool being delegated to
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_accountHistory", CamelToSnake]] AccountHistory
+
+instance ToSample AccountHistory where
+  toSamples  = pure $ samples
+    [ AccountHistory
+        { _accountHistoryActiveEpoch = 260
+        , _accountHistoryAmount = 1395265
+        , _accountHistoryPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+        }
+    , AccountHistory
+        { _accountHistoryActiveEpoch = 211
+        , _accountHistoryAmount = 22695385
+        , _accountHistoryPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+        }
+    ]
+
+-- | Account delegations and associated transaction IDs
+data AccountDelegation = AccountDelegation
+  { _accountDelegationActiveEpoch :: Epoch -- ^ Epoch in which the delegation becomes active
+  , _accountDelegationTxHash      :: TxHash -- ^ Hash of the transaction containing the delegation
+  , _accountDelegationAmount      :: Lovelaces -- ^ Rewards for given epoch in Lovelaces
+  , _accountDelegationPoolId      :: PoolId -- ^ Bech32 ID of pool being delegated to
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_accountDelegation", CamelToSnake]] AccountDelegation
+
+instance ToSample AccountDelegation where
+  toSamples = pure $ samples
+    [ AccountDelegation
+        { _accountDelegationActiveEpoch = 210
+        , _accountDelegationTxHash = "2dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531"
+        , _accountDelegationAmount = 12695385
+        , _accountDelegationPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+        }
+    , AccountDelegation
+        { _accountDelegationActiveEpoch = 242
+        , _accountDelegationTxHash = "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dde628516157f0"
+        , _accountDelegationAmount = 12691385
+        , _accountDelegationPoolId = "pool1kchver88u3kygsak8wgll7htr8uxn5v35lfrsyy842nkscrzyvj"
+        }
+    ]
+
+-- | Registration action
+data AccountRegistrationAction = Registered | Deregistered
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[ConstructorTagModifier '[ToLower]] AccountRegistrationAction
+
+instance ToSample AccountRegistrationAction where
+  toSamples = pure $ samples [ Registered, Deregistered ]
+
+-- | Account (de)registration
+data AccountRegistration = AccountRegistration
+  { _accountRegistrationAction :: AccountRegistrationAction -- ^ Action in the certificate
+  , _accountRegistrationTxHash :: TxHash -- ^ Hash of the transaction containing the (de)registration certificate
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_accountRegistration", CamelToSnake]] AccountRegistration
+
+instance ToSample AccountRegistration where
+  toSamples = pure $ samples
+    [ AccountRegistration
+        { _accountRegistrationAction = Registered
+        , _accountRegistrationTxHash = "2dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531"
+        }
+    , AccountRegistration
+        { _accountRegistrationAction = Deregistered
+        , _accountRegistrationTxHash = "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dde628516157f0"
+        }
+    ]
+
+-- | Withdrawal from an account
+data AccountWithdrawal = AccountWithdrawal
+  { _accountWithdrawalAmount :: Lovelaces -- ^ Withdrawal amount in Lovelaces
+  , _accountWithdrawalTxHash :: TxHash -- ^ Hash of the transaction containing the withdrawal
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_accountWithdrawal", CamelToSnake]] AccountWithdrawal
+
+instance ToSample AccountWithdrawal where
+  toSamples = pure $ samples
+    [ AccountWithdrawal
+        { _accountWithdrawalAmount = 454541212442
+        , _accountWithdrawalTxHash = "48a9625c841eea0dd2bb6cf551eabe6523b7290c9ce34be74eedef2dd8f7ecc5"
+        }
+    , AccountWithdrawal
+        { _accountWithdrawalAmount = 97846969
+        , _accountWithdrawalTxHash = "4230b0cbccf6f449f0847d8ad1d634a7a49df60d8c142bb8cc2dbc8ca03d9e34"
+        }
+    ]
+
+-- | Account MIR (Move Instantaneous Reward)
+data AccountMir = AccountMir
+  { _accountMirAmount :: Lovelaces -- ^ MIR amount in Lovelaces
+  , _accountMirTxHash :: TxHash -- ^ Hash of the transaction containing the MIR
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_accountMir", CamelToSnake]] AccountMir
+
+instance ToSample AccountMir where
+  toSamples = pure $ samples
+    [ AccountMir
+        { _accountMirAmount = 6202170
+        , _accountMirTxHash = "2dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531"
+        }
+    , AccountMir
+        { _accountMirAmount = 1202170
+        , _accountMirTxHash = "1dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531"
+        }
+    ]
diff --git a/src/Blockfrost/Types/Cardano/Addresses.hs b/src/Blockfrost/Types/Cardano/Addresses.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano/Addresses.hs
@@ -0,0 +1,143 @@
+-- | Responses for Cardano address queries
+
+module Blockfrost.Types.Cardano.Addresses
+  ( AddressInfo (..)
+  , AddressType (..)
+  , AddressDetails (..)
+  , AddressUTXO (..)
+  , AddressTransaction (..)
+  ) where
+
+import Blockfrost.Types.Shared
+import Deriving.Aeson
+import qualified Money
+import Servant.Docs (ToSample (..), samples, singleSample)
+
+-- | Information about Cardano address
+data AddressInfo = AddressInfo
+  { _addressInfoAddress      :: Address -- ^ Bech32 encoded addresses
+  , _addressInfoAmount       :: [Amount] -- ^ Lovelaces or tokens stored on this address
+  , _addressInfoStakeAddress :: Maybe Address -- ^ Stake address that controls the key
+  , _addressInfoType         :: AddressType -- ^ Address era
+  } deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_addressInfo", CamelToSnake]] AddressInfo
+
+instance ToSample AddressInfo where
+  toSamples = pure $ singleSample
+    AddressInfo
+      { _addressInfoAddress = "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz"
+      , _addressInfoAmount =
+        [ AdaAmount 42000000
+        , AssetAmount
+            $ Money.mkSomeDiscrete
+                "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+                unitScale
+                12
+        ]
+      , _addressInfoStakeAddress = pure "stake1ux3g2c9dx2nhhehyrezyxpkstartcqmu9hk63qgfkccw5rqttygt7"
+      , _addressInfoType = Shelley
+      }
+
+-- | Type (era) of an address
+data AddressType = Byron | Shelley
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[ConstructorTagModifier '[ToLower]] AddressType
+
+instance ToSample AddressType where
+  toSamples = pure $ samples [ Byron, Shelley ]
+
+-- | Details about Cardano address
+data AddressDetails = AddressDetails
+  { _addressDetailsAddress     :: Address -- ^ Bech32 encoded address
+  , _addressDetailsReceivedSum :: [Amount] -- ^ Total Lovelaces or tokens received by this address
+  , _addressDetailsSentSum     :: [Amount] -- ^ Total Lovelaces or tokens sent by this address
+  , _addressDetailsTxCount     :: Integer -- ^ Count of all transactions on the address
+  } deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_addressDetails", CamelToSnake]] AddressDetails
+
+instance ToSample AddressDetails where
+  toSamples = pure $ singleSample
+    AddressDetails
+      { _addressDetailsAddress = "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz"
+      , _addressDetailsReceivedSum = amounts
+      , _addressDetailsSentSum = amounts
+      , _addressDetailsTxCount = 12
+      }
+    where amounts =
+            [ AdaAmount 42000000
+            , AssetAmount
+                $ Money.mkSomeDiscrete
+                        "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+                        unitScale
+                        12
+            ]
+
+-- | UTxOs of the address
+data AddressUTXO = AddressUTXO
+  { _addressUTXOTxHash      :: TxHash -- ^ Transaction hash of the UTXO
+  , _addressUTXOOutputIndex :: Integer -- ^ UTXO index in the transaction
+  , _addressUTXOAmount      :: [Amount] -- ^ Amounts of Lovelaces or tokens
+  , _addressUTXOBlock       :: BlockHash -- ^ Block hash of the UTXO
+  } deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_addressUTXO", CamelToSnake]] AddressUTXO
+
+instance ToSample AddressUTXO where
+  toSamples = pure $ samples
+    [ AddressUTXO
+      { _addressUTXOTxHash = "39a7a284c2a0948189dc45dec670211cd4d72f7b66c5726c08d9b3df11e44d58"
+      , _addressUTXOOutputIndex = 0
+      , _addressUTXOAmount = [ AdaAmount 42000000 ]
+      , _addressUTXOBlock = "7eb8e27d18686c7db9a18f8bbcfe34e3fed6e047afaa2d969904d15e934847e6"
+      }
+    , AddressUTXO
+      { _addressUTXOTxHash = "4c4e67bafa15e742c13c592b65c8f74c769cd7d9af04c848099672d1ba391b49"
+      , _addressUTXOOutputIndex = 0
+      , _addressUTXOAmount = [ AdaAmount 729235000 ]
+      , _addressUTXOBlock = "953f1b80eb7c11a7ffcd67cbd4fde66e824a451aca5a4065725e5174b81685b7"
+      }
+    , AddressUTXO
+      { _addressUTXOTxHash = "768c63e27a1c816a83dc7b07e78af673b2400de8849ea7e7b734ae1333d100d2"
+      , _addressUTXOOutputIndex = 1
+      , _addressUTXOAmount =
+          [ AdaAmount 42000000
+          , AssetAmount
+              $ Money.mkSomeDiscrete
+                  "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+                   unitScale
+                   12
+          ]
+      , _addressUTXOBlock = "5c571f83fe6c784d3fbc223792627ccf0eea96773100f9aedecf8b1eda4544d7"
+      }
+    ]
+
+-- | Transactions on the address
+data AddressTransaction = AddressTransaction {
+    _addressTransactionTxHash      :: TxHash -- ^ Hash of the transaction
+  , _addressTransactionTxIndex     :: Integer -- ^ Transaction index within the block
+  , _addressTransactionBlockHeight :: Integer -- ^ Block height
+  } deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_addressTransaction", CamelToSnake]] AddressTransaction
+
+instance ToSample AddressTransaction where
+  toSamples = pure $ samples
+    [ AddressTransaction
+      { _addressTransactionTxHash = "8788591983aa73981fc92d6cddbbe643959f5a784e84b8bee0db15823f575a5b"
+      , _addressTransactionTxIndex = 6
+      , _addressTransactionBlockHeight = 69
+      }
+    , AddressTransaction
+      { _addressTransactionTxHash = "52e748c4dec58b687b90b0b40d383b9fe1f24c1a833b7395cdf07dd67859f46f"
+      , _addressTransactionTxIndex = 9
+      , _addressTransactionBlockHeight = 4547
+      }
+    , AddressTransaction
+      { _addressTransactionTxHash = "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b"
+      , _addressTransactionTxIndex = 0
+      , _addressTransactionBlockHeight = 564654
+      }
+    ]
diff --git a/src/Blockfrost/Types/Cardano/Assets.hs b/src/Blockfrost/Types/Cardano/Assets.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano/Assets.hs
@@ -0,0 +1,221 @@
+-- | Responses for Cardano asset queries
+
+module Blockfrost.Types.Cardano.Assets
+  ( AssetInfo (..)
+  , AssetDetails (..)
+  , AssetOnChainMetadata (..)
+  , AssetMetadata (..)
+  , AssetHistory (..)
+  , AssetAction (..)
+  , AssetTransaction (..)
+  , AssetAddress (..)
+  ) where
+
+import Blockfrost.Types.Shared
+import Data.Text (Text)
+import Deriving.Aeson
+import Servant.Docs (ToSample (..), samples, singleSample)
+
+-- | Asset information, result of listing assets
+data AssetInfo = AssetInfo
+  { _assetInfoAsset    :: Text
+  , _assetInfoQuantity :: Quantity
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_assetInfo", CamelToSnake]] AssetInfo
+
+instance ToSample AssetInfo where
+  toSamples = pure $ samples
+    [ AssetInfo
+        { _assetInfoAsset = "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+        , _assetInfoQuantity = 1
+        }
+    , AssetInfo
+        { _assetInfoAsset = "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e75d"
+        , _assetInfoQuantity = 100000
+        }
+    , AssetInfo
+        { _assetInfoAsset = "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad"
+        , _assetInfoQuantity = 18605647
+        }
+    ]
+
+-- | On-chain metadata stored in the minting transaction under label 721,
+-- community discussion around the standard ongoing at https://github.com/cardano-foundation/CIPs/pull/85
+data AssetOnChainMetadata = AssetOnChainMetadata
+  { _assetOnChainMetadataName  :: Text -- ^ Name of the asset
+  , _assetOnChainMetadataImage :: Text -- ^ URI of the associated asset
+  -- TODO: in schema has `additionalProperties: true`
+  -- so it can carry more arbitrary properties, keep as Value?
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_assetOnChainMetadata", CamelToSnake]] AssetOnChainMetadata
+
+instance ToSample AssetOnChainMetadata where
+  toSamples = pure $ singleSample
+    AssetOnChainMetadata
+      { _assetOnChainMetadataName = "My NFT token"
+      , _assetOnChainMetadataImage = "ipfs://ipfs/QmfKyJ4tuvHowwKQCbCHj4L5T3fSj8cjs7Aau8V7BWv226"
+      }
+
+-- | Asset metadata obtained from Cardano token registry
+-- https://github.com/cardano-foundation/cardano-token-registry
+data AssetMetadata = AssetMetadata
+  { _assetMetadataName        :: Text -- ^ Asset name
+  , _assetMetadataDescription :: Text -- ^ Asset description
+  , _assetMetadataTicker      :: Maybe Text
+  , _assetMetadataUrl         :: Maybe Text -- ^ Asset website
+  , _assetMetadataLogo        :: Maybe Text -- ^ Base64 encoded logo of the asset
+  , _assetMetadataDecimals    :: Maybe Int -- ^ Number of decimal places of the asset unit
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_assetMetadata", CamelToSnake]] AssetMetadata
+
+instance ToSample AssetMetadata where
+  toSamples = pure $ singleSample
+    AssetMetadata
+      { _assetMetadataName = "nutcoin"
+      , _assetMetadataDescription = "The Nut Coin"
+      , _assetMetadataTicker = pure "nutc"
+      , _assetMetadataUrl = pure "https://www.stakenuts.com/"
+      , _assetMetadataLogo =  pure "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"
+      , _assetMetadataDecimals = pure 6
+      }
+
+-- | Details of an asset
+data AssetDetails = AssetDetails
+  { _assetDetailsAsset             :: Text -- ^ Hex-encoded asset full name
+  , _assetDetailsPolicyId          :: PolicyId -- ^ Policy ID of the asset
+  , _assetDetailsAssetName         :: Maybe Text -- ^ Hex-encoded asset name of the asset
+  , _assetDetailsFingerprint       :: Text -- ^ CIP14 based user-facing fingerprint
+  , _assetDetailsQuantity          :: Quantity -- ^ Current asset quantity
+  , _assetDetailsInitialMintTxHash :: TxHash -- ^ ID of the initial minting transaction
+  , _assetDetailsMintOrBurnCount   :: Integer -- ^ Count of mint and burn transactions
+  , _assetDetailsOnchainMetadata   :: Maybe AssetOnChainMetadata
+  -- ^ On-chain metadata stored in the minting transaction under label 721,
+  -- community discussion around the standard ongoing at https://github.com/cardano-foundation/CIPs/pull/85
+  , _assetDetailsMetadata          :: Maybe AssetMetadata
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_assetDetails", CamelToSnake]] AssetDetails
+
+instance ToSample AssetDetails where
+  toSamples = pure $ singleSample
+    AssetDetails
+      { _assetDetailsAsset = "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+      , _assetDetailsPolicyId = "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7"
+      , _assetDetailsAssetName = pure "6e7574636f696e"
+      , _assetDetailsFingerprint = "asset1pkpwyknlvul7az0xx8czhl60pyel45rpje4z8w"
+      , _assetDetailsQuantity = 12000
+      , _assetDetailsInitialMintTxHash = "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad"
+      , _assetDetailsMintOrBurnCount = 1
+      , _assetDetailsOnchainMetadata = pure $
+          AssetOnChainMetadata
+            { _assetOnChainMetadataName = "My NFT token"
+            , _assetOnChainMetadataImage = "ipfs://ipfs/QmfKyJ4tuvHowwKQCbCHj4L5T3fSj8cjs7Aau8V7BWv226"
+            }
+      , _assetDetailsMetadata = pure $
+          AssetMetadata
+            { _assetMetadataName = "nutcoin"
+            , _assetMetadataDescription = "The Nut Coin"
+            , _assetMetadataTicker = pure "nutc"
+            , _assetMetadataUrl = pure "https://www.stakenuts.com/"
+            , _assetMetadataLogo =  pure "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"
+            , _assetMetadataDecimals = pure 6
+            }
+      }
+
+-- | Action of the asset.
+-- Created (`Minted`) or destroyed (`Burned`).
+data AssetAction = Minted | Burned
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[ConstructorTagModifier '[ToLower]] AssetAction
+
+instance ToSample AssetAction where
+  toSamples = pure $ samples [ Minted, Burned ]
+
+-- | History of an asset
+data AssetHistory = AssetHistory
+  { _assetHistoryTxHash :: TxHash -- ^ Hash of the transaction containing the asset action
+  , _assetHistoryAmount :: Quantity -- ^ Asset amount of the specific action
+  , _assetHistoryAction :: AssetAction -- ^ Action executed upon the asset policy
+  } deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_assetHistory", CamelToSnake]] AssetHistory
+
+instance ToSample AssetHistory where
+  toSamples = pure $ samples
+    [ AssetHistory
+        { _assetHistoryTxHash = "2dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531"
+        , _assetHistoryAmount = 10
+        , _assetHistoryAction = Minted
+        }
+    , AssetHistory
+        { _assetHistoryTxHash = "9c190bc1ac88b2ab0c05a82d7de8b71b67a9316377e865748a89d4426c0d3005"
+        , _assetHistoryAmount = 5
+        , _assetHistoryAction = Burned
+        }
+    , AssetHistory
+        { _assetHistoryTxHash = "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dde628516157f0"
+        , _assetHistoryAmount = 5
+        , _assetHistoryAction = Burned
+        }
+    ]
+
+-- | Transaction of an asset
+data AssetTransaction = AssetTransaction
+  { _assetTransactionTxHash      :: TxHash -- ^ Hash of the transaction
+  , _assetTransactionTxIndex     :: Integer -- ^ Transaction index within the block
+  , _assetTransactionBlockHeight :: Integer -- ^ Block height
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_assetTransaction", CamelToSnake]] AssetTransaction
+
+instance ToSample AssetTransaction where
+  toSamples = pure $ samples
+    [ AssetTransaction
+        { _assetTransactionTxHash = "8788591983aa73981fc92d6cddbbe643959f5a784e84b8bee0db15823f575a5b"
+        , _assetTransactionTxIndex = 6
+        , _assetTransactionBlockHeight = 69
+        }
+    , AssetTransaction
+        { _assetTransactionTxHash = "52e748c4dec58b687b90b0b40d383b9fe1f24c1a833b7395cdf07dd67859f46f"
+        , _assetTransactionTxIndex = 9
+        , _assetTransactionBlockHeight = 4547
+        }
+     , AssetTransaction
+        { _assetTransactionTxHash = "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b"
+        , _assetTransactionTxIndex = 0
+        , _assetTransactionBlockHeight = 564654
+        }
+    ]
+
+-- | An address containing specific asset
+data AssetAddress = AssetAddress
+  { _assetAddressAddress  :: Address -- ^ Address containing the specific asset
+  , _assetAddressQuantity :: Quantity -- ^ Asset quantity on the specific address
+  } deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_assetAddress", CamelToSnake]] AssetAddress
+
+instance ToSample AssetAddress where
+  toSamples = pure $ samples
+    [ AssetAddress
+        { _assetAddressAddress = "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz"
+        , _assetAddressQuantity = 1
+        }
+    , AssetAddress
+        { _assetAddressAddress = "addr1qyhr4exrgavdcn3qhfcc9f939fzsch2re5ry9cwvcdyh4x4re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qdpvhza"
+        , _assetAddressQuantity = 100000
+        }
+     , AssetAddress
+        { _assetAddressAddress = "addr1q8zup8m9ue3p98kxlxl9q8rnyan8hw3ul282tsl9s326dfj088lvedv4zckcj24arcpasr0gua4c5gq4zw2rpcpjk2lq8cmd9l"
+        , _assetAddressQuantity = 18605647
+        }
+    ]
diff --git a/src/Blockfrost/Types/Cardano/Blocks.hs b/src/Blockfrost/Types/Cardano/Blocks.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano/Blocks.hs
@@ -0,0 +1,52 @@
+-- | Information about produced blocks
+
+module Blockfrost.Types.Cardano.Blocks
+  ( Block (..)
+  ) where
+
+import Data.Text (Text)
+import Deriving.Aeson
+import Servant.Docs (ToSample (..), singleSample)
+
+import Blockfrost.Types.Shared
+
+-- | Information about a block
+data Block = Block
+  { _blockTime          :: POSIXTime -- ^ Block creation time in UNIX time
+  , _blockHeight        :: Maybe Integer -- ^ Block number
+  , _blockHash          :: BlockHash -- ^ Hash of the block
+  , _blockSlot          :: Maybe Slot -- ^ Slot number
+  , _blockEpoch         :: Maybe Epoch -- ^ Epoch number
+  , _blockEpochSlot     :: Maybe Integer -- ^ Slot within the epoch
+  , _blockSlotLeader    :: Text -- ^ Bech32 ID of the slot leader or specific block description in case there is no slot leader
+  , _blockSize          :: Integer -- ^ Block size in Bytes
+  , _blockTxCount       :: Integer -- ^ Number of transactions in the block
+  , _blockOutput        :: Maybe Lovelaces -- ^ Total output within the block in Lovelaces
+  , _blockFees          :: Maybe Lovelaces -- ^ Total fees within the block in Lovelaces
+  , _blockBlockVrf      :: Maybe Text -- ^ VRF key of the block
+  , _blockPreviousBlock :: Maybe BlockHash -- ^ Hash of the previous block
+  , _blockNextBlock     :: Maybe BlockHash -- ^ Hash of the next block
+  , _blockConfirmations :: Integer -- ^ Number of block confirmations
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_block", CamelToSnake]] Block
+
+instance ToSample Block where
+  toSamples _ = singleSample $ Block
+    { _blockTime = 1641338934
+    , _blockHeight = pure 15243593
+    , _blockHash = "4ea1ba291e8eef538635a53e59fddba7810d1679631cc3aed7c8e6c4091a516a"
+    , _blockSlot = pure 412162133
+    , _blockEpoch = pure 425
+    , _blockEpochSlot = pure 12
+    , _blockSlotLeader = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2qnikdy"
+    , _blockSize = 3
+    , _blockTxCount = 1
+    , _blockOutput = pure 128314491794
+    , _blockFees = pure 592661
+    , _blockBlockVrf = pure "vrf_vk1wf2k6lhujezqcfe00l6zetxpnmh9n6mwhpmhm0dvfh3fxgmdnrfqkms8ty"
+    , _blockPreviousBlock = pure "43ebccb3ac72c7cebd0d9b755a4b08412c9f5dcb81b8a0ad1e3c197d29d47b05"
+    , _blockNextBlock = pure "8367f026cf4b03e116ff8ee5daf149b55ba5a6ec6dec04803b8dc317721d15fa"
+    , _blockConfirmations = 4698
+    }
diff --git a/src/Blockfrost/Types/Cardano/Epochs.hs b/src/Blockfrost/Types/Cardano/Epochs.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano/Epochs.hs
@@ -0,0 +1,133 @@
+-- | Responses for Cardano epoch quries
+
+module Blockfrost.Types.Cardano.Epochs
+  ( EpochInfo (..)
+  , PoolStakeDistribution (..)
+  , ProtocolParams (..)
+  , StakeDistribution (..)
+  ) where
+
+import Blockfrost.Types.Shared
+import Data.Text (Text)
+import Deriving.Aeson
+import Servant.Docs (ToSample (..), singleSample)
+
+-- | Information about an epoch
+data EpochInfo = EpochInfo
+  { _epochInfoEpoch          :: Epoch -- ^ Epoch number
+  , _epochInfoStartTime      :: POSIXTime -- ^ Unix time of the start of the epoch
+  , _epochInfoEndTime        :: POSIXTime -- ^ Unix time of the end of the epoch
+  , _epochInfoFirstBlockTime :: POSIXTime -- ^ Unix time of the first block of the epoch
+  , _epochInfoLastBlockTime  :: POSIXTime -- ^ Unix time of the last block of the epoch
+  , _epochInfoBlockCount     :: Integer -- ^ Number of blocks within the epoch
+  , _epochInfoTxCount        :: Integer -- ^ Number of transactions within the epoch
+  , _epochInfoOutput         :: Lovelaces -- ^ Sum of all the transactions within the epoch in Lovelaces
+  , _epochInfoFees           :: Lovelaces -- ^ Sum of all the fees within the epoch in Lovelaces
+  , _epochInfoActiveStake    :: Maybe Lovelaces -- ^ Sum of all the active stakes within the epoch in Lovelaces
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_epochInfo", CamelToSnake]] EpochInfo
+
+instance ToSample EpochInfo where
+  toSamples = pure $ singleSample
+    EpochInfo
+      { _epochInfoEpoch = 225
+      , _epochInfoStartTime = 1603403091
+      , _epochInfoEndTime = 1603835086
+      , _epochInfoFirstBlockTime = 1603403092
+      , _epochInfoLastBlockTime = 1603835084
+      , _epochInfoBlockCount = 21298
+      , _epochInfoTxCount = 17856
+      , _epochInfoOutput = 7849943934049314
+      , _epochInfoFees = 4203312194
+      , _epochInfoActiveStake = pure 784953934049314
+      }
+
+
+-- | Protocol parameters
+data ProtocolParams = ProtocolParams
+  { _protocolParamsEpoch                 :: Epoch -- ^ Epoch number
+  , _protocolParamsMinFeeA               :: Integer -- ^ The linear factor for the minimum fee calculation for given epoch
+  , _protocolParamsMinFeeB               :: Integer -- ^ The constant factor for the minimum fee calculation
+  , _protocolParamsMaxBlockSize          :: Integer -- ^ Maximum block body size in Bytes
+  , _protocolParamsMaxTxSize             :: Integer -- ^ Maximum transaction size
+  , _protocolParamsMaxBlockHeaderSize    :: Integer -- ^ Maximum block header size
+  , _protocolParamsKeyDeposit            :: Lovelaces -- ^ The amount of a key registration deposit in Lovelaces
+  , _protocolParamsPoolDeposit           :: Lovelaces -- ^ The amount of a pool registration deposit in Lovelaces
+  , _protocolParamsEMax                  :: Integer -- ^ Epoch bound on pool retirement
+  , _protocolParamsNOpt                  :: Integer -- ^ Desired number of pools
+  , _protocolParamsA0                    :: Double -- ^ Pool pledge influence
+  , _protocolParamsRho                   :: Double -- ^ Monetary expansion
+  , _protocolParamsTau                   :: Double -- ^ Treasury expansion
+  , _protocolParamsDecentralisationParam :: Double -- ^ Percentage of blocks produced by federated nodes
+-- ?? TODO: object Nullable
+--  , protocolParamsExtraEntropy :: Maybe Value
+  , _protocolParamsProtocolMajorVer      :: Integer -- ^ Accepted protocol major version
+  , _protocolParamsProtocolMinorVer      :: Integer -- ^ Accepted protocol minor version
+  , _protocolParamsMinUtxo               :: Lovelaces -- ^ Minimum UTXO value
+  , _protocolParamsMinPoolCost           :: Lovelaces  -- ^ Minimum stake cost forced on the pool
+  , _protocolParamsNonce                 :: Text -- ^ Epoch number only used once
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_protocolParams", CamelToSnake]] ProtocolParams
+
+instance ToSample ProtocolParams where
+  toSamples = pure $ singleSample
+    ProtocolParams
+      { _protocolParamsEpoch = 225
+      , _protocolParamsMinFeeA = 44
+      , _protocolParamsMinFeeB = 155381
+      , _protocolParamsMaxBlockSize = 65536
+      , _protocolParamsMaxTxSize = 16384
+      , _protocolParamsMaxBlockHeaderSize = 1100
+      , _protocolParamsKeyDeposit = 2000000
+      , _protocolParamsPoolDeposit = 500000000
+      , _protocolParamsEMax = 18
+      , _protocolParamsNOpt = 150
+      , _protocolParamsA0 = 0.3
+      , _protocolParamsRho = 0.003
+      , _protocolParamsTau = 0.2
+      , _protocolParamsDecentralisationParam = 0.5
+--      , _protocolParamsExtraEntropy = Nothing
+      , _protocolParamsProtocolMajorVer = 2
+      , _protocolParamsProtocolMinorVer = 0
+      , _protocolParamsMinUtxo = 1000000
+      , _protocolParamsMinPoolCost = 340000000
+      , _protocolParamsNonce = "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81"
+      }
+
+-- | Active stake distribution for an epoch
+data StakeDistribution = StakeDistribution
+  { _stakeDistributionStakeAddress :: Address -- ^ Stake address
+  , _stakeDistributionPoolId       :: PoolId -- ^ Bech32 prefix of the pool delegated to
+  , _stakeDistributionAmount       :: Lovelaces -- ^ Amount of active delegated stake in Lovelaces
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_stakeDistribution", CamelToSnake]] StakeDistribution
+
+instance ToSample StakeDistribution where
+  toSamples = pure $ singleSample
+    StakeDistribution
+      { _stakeDistributionStakeAddress = "stake1u9l5q5jwgelgagzyt6nuaasefgmn8pd25c8e9qpeprq0tdcp0e3uk"
+      , _stakeDistributionPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      , _stakeDistributionAmount = 4440295078
+      }
+
+-- | Stake distribution for an epoch for specific pool
+data PoolStakeDistribution = PoolStakeDistribution
+  { _poolStakeDistributionStakeAddress :: Address -- ^ Stake address
+  , _poolStakeDistributionAmount       :: Lovelaces -- ^ Amount of active delegated stake in Lovelaces
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_poolStakeDistribution", CamelToSnake]] PoolStakeDistribution
+
+instance ToSample PoolStakeDistribution where
+  toSamples = pure $ singleSample
+    PoolStakeDistribution
+      { _poolStakeDistributionStakeAddress = "stake1u9l5q5jwgelgagzyt6nuaasefgmn8pd25c8e9qpeprq0tdcp0e3uk"
+      , _poolStakeDistributionAmount = 4440295078
+      }
diff --git a/src/Blockfrost/Types/Cardano/Genesis.hs b/src/Blockfrost/Types/Cardano/Genesis.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano/Genesis.hs
@@ -0,0 +1,44 @@
+-- | Blockchain genesis
+
+{-# LANGUAGE NumericUnderscores #-}
+
+module Blockfrost.Types.Cardano.Genesis
+  ( Genesis (..)
+  ) where
+
+import Deriving.Aeson
+import Servant.Docs (ToSample (..), singleSample)
+
+import Blockfrost.Types.Shared
+
+-- | Information about blockchain genesis
+data Genesis = Genesis
+  { _genesisActiveSlotsCoefficient :: Double -- ^ The proportion of slots in which blocks should be issued
+  , _genesisUpdateQuorum           :: Integer -- ^ Determines the quorum needed for votes on the protocol parameter updates
+  , _genesisMaxLovelaceSupply      :: Lovelaces -- ^ The total number of lovelace in the system
+  , _genesisNetworkMagic           :: Integer -- ^ Network identifier
+  , _genesisEpochLength            :: Integer -- ^ Number of slots in an epoch
+  , _genesisSystemStart            :: POSIXTime -- ^ Time of slot 0 in UNIX time
+  , _genesisSlotsPerKesPeriod      :: Integer -- ^ Number of slots in an KES period
+  , _genesisSlotLength             :: Integer -- ^ Duration of one slot in seconds
+  , _genesisMaxKesEvolutions       :: Integer -- ^ The maximum number of time a KES key can be evolved before a pool operator must create a new operational certificate
+  , _genesisSecurityParam          :: Integer -- ^ Security parameter @k@
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_genesis", CamelToSnake]] Genesis
+
+instance ToSample Genesis where
+  toSamples _ = singleSample $
+    Genesis
+      { _genesisActiveSlotsCoefficient = 0.05
+      , _genesisUpdateQuorum = 5
+      , _genesisMaxLovelaceSupply = 45_000_000_000_000_000
+      , _genesisNetworkMagic = 764824073
+      , _genesisEpochLength = 432_000
+      , _genesisSystemStart = 1506203091
+      , _genesisSlotsPerKesPeriod = 129600
+      , _genesisSlotLength = 1
+      , _genesisMaxKesEvolutions  = 62
+      , _genesisSecurityParam  = 2160
+      }
diff --git a/src/Blockfrost/Types/Cardano/Metadata.hs b/src/Blockfrost/Types/Cardano/Metadata.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano/Metadata.hs
@@ -0,0 +1,83 @@
+-- | Transaction metadata
+
+module Blockfrost.Types.Cardano.Metadata
+  ( TxMeta (..)
+  , TxMetaJSON (..)
+  , TxMetaCBOR (..)
+  ) where
+
+import Data.Aeson (Value, object, (.=))
+import Data.Text (Text)
+import Deriving.Aeson
+import Servant.Docs (ToSample (..), samples)
+
+import Blockfrost.Types.Shared
+
+-- | Transaction metadata label in use
+data TxMeta = TxMeta
+  { _txMetaLabel :: Text -- ^ Metadata label
+  , _txMetaCip10 :: Maybe Text -- ^ CIP10 defined description
+  , _txMetaCount :: Quantity -- ^ The count of metadata entries with a specific label
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_txMeta", CamelToSnake]] TxMeta
+
+instance ToSample TxMeta where
+  toSamples = pure $ samples
+    [ TxMeta "1990" Nothing 1
+    , TxMeta "1967" (Just "nut.link metadata oracles registry") 3
+    , TxMeta "1968" (Just "nut.link metadata oracles data points") 16321
+    ]
+
+-- | Transaction metadata content in JSON
+data TxMetaJSON = TxMetaJSON
+  { _txMetaJSONTxHash       :: Text -- ^ Transaction hash that contains the specific metadata
+  , _txMetaJSONJSONMetadata :: Maybe Value -- ^ Content of the JSON metadata
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_txMetaJSON", CamelToSnake]] TxMetaJSON
+
+instance ToSample TxMetaJSON where
+  toSamples =
+    let oracleMeta val =
+          object [
+            "ADAUSD" .=
+              [ object [ "value" .= (val :: Text)
+                       , "source" .= ("ergoOracles" :: Text) ]
+              ]
+          ]
+    in pure $ samples
+    [ TxMetaJSON
+        "257d75c8ddb0434e9b63e29ebb6241add2b835a307aa33aedba2effe09ed4ec8"
+        (Just $ oracleMeta "0.10409800535729975")
+    , TxMetaJSON
+        "e865f2cc01ca7381cf98dcdc4de07a5e8674b8ea16e6a18e3ed60c186fde2b9c"
+        (Just $ oracleMeta "0.15409850555139935")
+    , TxMetaJSON
+        "4237501da3cfdd53ade91e8911e764bd0699d88fd43b12f44a1f459b89bc91be"
+        Nothing
+    ]
+
+-- | Transaction metadata content in CBOR
+data TxMetaCBOR = TxMetaCBOR
+  { _txMetaCBORTxHash       :: Text -- ^ Transaction hash that contains the specific metadata
+  , _txMetaCBORCBORMetadata :: Maybe Text -- ^ Content of the CBOR metadata
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_txMetaCBOR", CamelToSnake]] TxMetaCBOR
+
+instance ToSample TxMetaCBOR where
+  toSamples = pure $ samples
+    [ TxMetaCBOR
+        "257d75c8ddb0434e9b63e29ebb6241add2b835a307aa33aedba2effe09ed4ec8"
+        Nothing
+    , TxMetaCBOR
+        "e865f2cc01ca7381cf98dcdc4de07a5e8674b8ea16e6a18e3ed60c186fde2b9c"
+        Nothing
+    , TxMetaCBOR
+        "4237501da3cfdd53ade91e8911e764bd0699d88fd43b12f44a1f459b89bc91be"
+        (Just "\\xa100a16b436f6d62696e6174696f6e8601010101010c")
+    ]
diff --git a/src/Blockfrost/Types/Cardano/Network.hs b/src/Blockfrost/Types/Cardano/Network.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano/Network.hs
@@ -0,0 +1,65 @@
+-- | Cardano Network reponses
+
+{-# LANGUAGE NumericUnderscores #-}
+
+module Blockfrost.Types.Cardano.Network
+  ( Network (..)
+  , NetworkStake (..)
+  , NetworkSupply (..)
+  ) where
+
+import Deriving.Aeson
+import Servant.Docs (ToSample (..), singleSample)
+
+import Blockfrost.Types.Shared
+
+-- | Lovelace supply data
+data NetworkSupply = NetworkSupply
+  { _supplyMax         :: Lovelaces -- ^ Maximum supply in Lovelaces
+  , _supplyTotal       :: Lovelaces -- ^ Current total (max supply - reserves) supply in Lovelaces
+  , _supplyCirculating :: Lovelaces -- ^ Current circulating (UTXOs + withdrawables) supply in Lovelaces
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_supply", CamelToSnake]] NetworkSupply
+
+netSupplySample :: NetworkSupply
+netSupplySample =
+  NetworkSupply
+    45_000_000_000_000_000
+    32_890_715_183_299_160
+    32_412_601_976_210_393
+
+instance ToSample NetworkSupply where
+  toSamples = pure $ singleSample netSupplySample
+
+-- | Live and active stake of the whole network
+data NetworkStake = NetworkStake
+  { _stakeLive   :: Lovelaces -- ^ Current live stake in Lovelaces
+  , _stakeActive :: Lovelaces -- ^ Current active stake in Lovelaces
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_stake", CamelToSnake]] NetworkStake
+
+netStakeSample :: NetworkStake
+netStakeSample =
+  NetworkStake
+    23_204_950_463_991_654
+    22_210_233_523_456_321
+
+instance ToSample NetworkStake where
+  toSamples = pure $ singleSample $ netStakeSample
+
+-- | Detailed network information
+data Network = Network
+  { _networkSupply :: NetworkSupply -- ^ Supply data
+  , _networkStake  :: NetworkStake -- ^ Stake data
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_network", CamelToSnake]] Network
+
+instance ToSample Network where
+  toSamples = pure $ singleSample $
+    Network netSupplySample netStakeSample
diff --git a/src/Blockfrost/Types/Cardano/Pools.hs b/src/Blockfrost/Types/Cardano/Pools.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano/Pools.hs
@@ -0,0 +1,254 @@
+-- | Cardano Pools reponses
+
+module Blockfrost.Types.Cardano.Pools
+  ( PoolEpoch (..)
+  , PoolInfo (..)
+  , PoolHistory (..)
+  , PoolMetadata (..)
+  , PoolRelay (..)
+  , PoolDelegator (..)
+  , PoolUpdate (..)
+  , PoolRegistrationAction (..)
+  , samplePoolRelay
+  ) where
+
+import Data.Aeson (FromJSON (..), ToJSON (..), object, pairs, withText)
+import Data.Text (Text)
+import Deriving.Aeson
+import Servant.Docs (ToSample (..), samples, singleSample)
+
+import Blockfrost.Types.Shared
+
+-- | Retirement epoch for pool
+data PoolEpoch = PoolEpoch
+  { _poolEpochPoolId :: PoolId -- ^ Bech32 encoded pool ID
+  , _poolEpochEpoch  :: Epoch -- ^ Retirement epoch number
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_poolEpoch", CamelToSnake]] PoolEpoch
+
+instance ToSample PoolEpoch where
+  toSamples = pure $ samples
+    [ PoolEpoch "pool19u64770wqp6s95gkajc8udheske5e6ljmpq33awxk326zjaza0q" 225
+    , PoolEpoch "pool1dvla4zq98hpvacv20snndupjrqhuc79zl6gjap565nku6et5zdx" 215
+    , PoolEpoch "pool1wvccajt4eugjtf3k0ja3exjqdj7t8egsujwhcw4tzj4rzsxzw5w" 231
+    ]
+
+-- | Detailed pool information
+data PoolInfo = PoolInfo
+  { _poolInfoPoolId         :: PoolId -- ^ Bech32 encoded pool ID
+  , _poolInfoHex            :: Text -- ^ Hexadecimal pool ID.
+  , _poolInfoVrfKey         :: Text -- ^ VRF key hash
+  , _poolInfoBlocksMinted   :: Integer -- ^ Total minted blocks
+  , _poolInfoLiveStake      :: Lovelaces
+  , _poolInfoLiveSize       :: Double
+  , _poolInfoLiveSaturation :: Double
+  , _poolInfoActiveStake    :: Lovelaces
+  , _poolInfoActiveSize     :: Double
+  , _poolInfoDeclaredPledge :: Lovelaces -- ^ Stake pool certificate pledge
+  , _poolInfoLivePledge     :: Lovelaces -- ^ Stake pool current pledge
+  , _poolInfoMarginCost     :: Double -- ^ Margin tax cost of the stake pool
+  , _poolInfoFixedCost      :: Lovelaces -- ^ Fixed tax cost of the stake pool
+  , _poolInfoRewardAccount  :: Address -- ^ Bech32 reward account of the stake pool
+  , _poolInfoOwners         :: [Address]
+  , _poolInfoRegistration   :: [Text]
+  , _poolInfoRetirement     :: [Text]
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_poolInfo", CamelToSnake]] PoolInfo
+
+instance ToSample PoolInfo where
+  toSamples = pure $ singleSample
+    PoolInfo
+      { _poolInfoPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      , _poolInfoHex = "0f292fcaa02b8b2f9b3c8f9fd8e0bb21abedb692a6d5058df3ef2735"
+      , _poolInfoVrfKey = "0b5245f9934ec2151116fb8ec00f35fd00e0aa3b075c4ed12cce440f999d8233"
+      , _poolInfoBlocksMinted = 69
+      , _poolInfoLiveStake = 6900000000
+      , _poolInfoLiveSize = 0.42
+      , _poolInfoLiveSaturation = 0.93
+      , _poolInfoActiveStake = 4200000000
+      , _poolInfoActiveSize = 0.43
+      , _poolInfoDeclaredPledge = 5000000000
+      , _poolInfoLivePledge = 5000000001
+      , _poolInfoMarginCost = 0.05
+      , _poolInfoFixedCost = 340000000
+      , _poolInfoRewardAccount = "stake1uxkptsa4lkr55jleztw43t37vgdn88l6ghclfwuxld2eykgpgvg3f"
+      , _poolInfoOwners = [ "stake1u98nnlkvkk23vtvf9273uq7cph5ww6u2yq2389psuqet90sv4xv9v" ]
+      , _poolInfoRegistration =
+          [ "9f83e5484f543e05b52e99988272a31da373f3aab4c064c76db96643a355d9dc"
+          , "7ce3b8c433bf401a190d58c8c483d8e3564dfd29ae8633c8b1b3e6c814403e95"
+          , "3e6e1200ce92977c3fe5996bd4d7d7e192bcb7e231bc762f9f240c76766535b9"
+          ]
+      , _poolInfoRetirement = [ "252f622976d39e646815db75a77289cf16df4ad2b287dd8e3a889ce14c13d1a8" ]
+      }
+
+-- | History of a stake pool parameters over epochs
+data PoolHistory = PoolHistory
+  { _poolHistoryEpoch           :: Epoch -- ^ Epoch number
+  , _poolHistoryBlocks          :: Integer -- ^ Number of blocks created by pool
+  , _poolHistoryActiveStake     :: Lovelaces -- ^ Active (Snapshot of live stake 2 epochs ago) stake in Lovelaces
+  , _poolHistoryActiveSize      :: Double -- ^ Pool size (percentage) of overall active stake at that epoch
+  , _poolHistoryDelegatorsCount :: Integer -- ^ Number of delegators for epoch
+  , _poolHistoryRewards         :: Lovelaces -- ^ Total rewards received before distribution to delegators
+  , _poolHistoryFees            :: Lovelaces -- ^ Pool operator rewards
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_poolHistory", CamelToSnake]] PoolHistory
+
+instance ToSample PoolHistory where
+  toSamples = pure $ singleSample
+    PoolHistory
+      { _poolHistoryEpoch = 233
+      , _poolHistoryBlocks = 22
+      , _poolHistoryActiveStake = 20485965693569
+      , _poolHistoryActiveSize = 1.2345
+      , _poolHistoryDelegatorsCount = 115
+      , _poolHistoryRewards = 206936253674159
+      , _poolHistoryFees = 1290968354
+      }
+
+-- | Stake pool registration metadata
+data PoolMetadata = PoolMetadata
+  { _poolMetadataPoolId      :: PoolId -- ^ Bech32 pool ID
+  , _poolMetadataHex         :: Text -- ^ Hexadecimal pool ID
+  , _poolMetadataUrl         :: Maybe Text -- ^ URL to the stake pool metadata
+  , _poolMetadataHash        :: Maybe Text -- ^ Hash of the metadata file
+  , _poolMetadataTicker      :: Maybe Text -- ^ Ticker of the stake pool
+  , _poolMetadataName        :: Maybe Text -- ^ Name of the stake pool
+  , _poolMetadataDescription :: Maybe Text -- ^ Description of the stake pool
+  , _poolMetadataHomepage    :: Maybe Text -- ^ Home page of the stake pool
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_poolMetadata", CamelToSnake]] PoolMetadata
+
+-- We need this more specific
+-- instance since API returns
+-- empty object if there's no metadata
+instance {-# OVERLAPS #-} ToJSON (Maybe PoolMetadata) where
+  toJSON Nothing   = object mempty
+  toJSON (Just pm) = toJSON pm
+  toEncoding Nothing   = pairs mempty
+  toEncoding (Just pm) = toEncoding pm
+instance {-# OVERLAPS #-} FromJSON (Maybe PoolMetadata) where
+  parseJSON x | x == object [] = pure Nothing
+  parseJSON x = Just <$> parseJSON x
+
+instance ToSample PoolMetadata where
+  toSamples = pure $ singleSample samplePoolMetadata
+
+samplePoolMetadata :: PoolMetadata
+samplePoolMetadata =
+  PoolMetadata
+    { _poolMetadataPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    , _poolMetadataHex = "0f292fcaa02b8b2f9b3c8f9fd8e0bb21abedb692a6d5058df3ef2735"
+    , _poolMetadataUrl = Just "https://stakenuts.com/mainnet.json"
+    , _poolMetadataHash = Just "47c0c68cb57f4a5b4a87bad896fc274678e7aea98e200fa14a1cb40c0cab1d8c"
+    , _poolMetadataTicker = Just "NUTS"
+    , _poolMetadataName = Just "Stake Nuts"
+    , _poolMetadataDescription = Just "The best pool ever"
+    , _poolMetadataHomepage = Just "https://stakentus.com/"
+    }
+
+-- | Relays of a stake pool
+data PoolRelay = PoolRelay
+  { _poolRelayIpv4   :: Maybe Text -- ^ IPv4 address of the relay
+  , _poolRelayIpv6   :: Maybe Text -- ^ IPv6 address of the relay
+  , _poolRelayDns    :: Maybe Text -- ^ DNS name of the relay
+  , _poolRelayDnsSrv :: Maybe Text -- ^ DNS SRV entry of the relay
+  , _poolRelayPort   :: Integer -- ^ Network port of the relay
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_poolRelay", CamelToSnake]] PoolRelay
+
+instance ToSample PoolRelay where
+  toSamples = pure $ singleSample samplePoolRelay
+
+-- | Example of `PoolRelay`
+samplePoolRelay :: PoolRelay
+samplePoolRelay =
+  PoolRelay
+    { _poolRelayIpv4 = Just "4.4.4.4"
+    , _poolRelayIpv6 = Just "https://stakenuts.com/mainnet.json"
+    , _poolRelayDns = Just "relay1.stakenuts.com"
+    , _poolRelayDnsSrv = Just "_relays._tcp.relays.stakenuts.com"
+    , _poolRelayPort = 3001
+    }
+
+-- | Stake pool delegator
+data PoolDelegator = PoolDelegator
+  { _poolDelegatorAddress   :: Text -- ^ Bech32 encoded stake addresses
+  , _poolDelegatorLiveStake :: Lovelaces -- ^ Currently delegated amount
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_poolDelegator", CamelToSnake]] PoolDelegator
+
+instance ToSample PoolDelegator where
+  toSamples = pure $ samples
+    [ PoolDelegator
+        { _poolDelegatorAddress = "stake1ux4vspfvwuus9uwyp5p3f0ky7a30jq5j80jxse0fr7pa56sgn8kha"
+        , _poolDelegatorLiveStake = 1137959159981411
+        }
+    , PoolDelegator
+        { _poolDelegatorAddress = "stake1uylayej7esmarzd4mk4aru37zh9yz0luj3g9fsvgpfaxulq564r5u"
+        , _poolDelegatorLiveStake = 16958865648
+        }
+    , PoolDelegator
+        { _poolDelegatorAddress = "stake1u8lr2pnrgf8f7vrs9lt79hc3sxm8s2w4rwvgpncks3axx6q93d4ck"
+        , _poolDelegatorLiveStake = 18605647
+        }
+    ]
+
+-- | Registration action of a pool
+data PoolRegistrationAction = PoolRegistered | PoolDeregistered
+  deriving stock (Show, Eq, Generic)
+
+instance ToJSON PoolRegistrationAction where
+  toJSON PoolRegistered   = toJSON ("registered" :: Text)
+  toJSON PoolDeregistered = toJSON ("deregistered" :: Text)
+  toEncoding PoolRegistered   = toEncoding ("registered" :: Text)
+  toEncoding PoolDeregistered = toEncoding ("deregistered" :: Text)
+
+instance FromJSON PoolRegistrationAction where
+  parseJSON = withText "action" $ \case
+    "registered"   -> pure PoolRegistered
+    "deregistered" -> pure PoolDeregistered
+    x              -> fail ("Expected registration action got " ++ show x)
+
+instance ToSample PoolRegistrationAction where
+  toSamples = pure $ samples [ PoolRegistered, PoolDeregistered ]
+
+-- | Certificate update to the stake pool
+data PoolUpdate = PoolUpdate
+  { _poolUpdateTxHash    :: TxHash -- ^ Transaction ID
+  , _poolUpdateCertIndex :: Integer -- ^ Certificate within the transaction
+  , _poolUpdateAction    :: PoolRegistrationAction -- ^ Action in the certificate
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_poolUpdate", CamelToSnake]] PoolUpdate
+
+instance ToSample PoolUpdate where
+  toSamples = pure $ samples
+    [ PoolUpdate
+        { _poolUpdateTxHash = "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad"
+        , _poolUpdateCertIndex = 0
+        , _poolUpdateAction = PoolRegistered
+        }
+    , PoolUpdate
+        { _poolUpdateTxHash = "9c190bc1ac88b2ab0c05a82d7de8b71b67a9316377e865748a89d4426c0d3005"
+        , _poolUpdateCertIndex = 0
+        , _poolUpdateAction = PoolDeregistered
+        }
+    , PoolUpdate
+        { _poolUpdateTxHash = "e14a75b0eb2625de7055f1f580d70426311b78e0d36dd695a6bdc96c7b3d80e0"
+        , _poolUpdateCertIndex = 1
+        , _poolUpdateAction = PoolRegistered
+        }
+    ]
diff --git a/src/Blockfrost/Types/Cardano/Transactions.hs b/src/Blockfrost/Types/Cardano/Transactions.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Cardano/Transactions.hs
@@ -0,0 +1,359 @@
+-- | Cardano Transactions responses
+
+module Blockfrost.Types.Cardano.Transactions
+  ( Transaction (..)
+  , TransactionUtxos (..)
+  , UtxoInput (..)
+  , UtxoOutput (..)
+  , TransactionStake (..)
+  , TransactionDelegation (..)
+  , TransactionWithdrawal (..)
+  , Pot (..)
+  , TransactionMir (..)
+  , TransactionPoolUpdate (..)
+  , PoolUpdateMetadata (..)
+  , TransactionPoolRetiring (..)
+  , TransactionMetaJSON (..)
+  , TransactionMetaCBOR (..)
+  ) where
+
+import Data.Aeson (Value, object, (.=))
+import Data.Text (Text)
+import Deriving.Aeson
+import qualified Money
+import Servant.Docs (ToSample (..), samples, singleSample)
+
+import Blockfrost.Types.Cardano.Pools
+import Blockfrost.Types.Shared
+
+-- | Information about a transaction
+data Transaction = Transaction
+  { _transactionHash                 :: Text -- ^ Transaction hash
+  , _transactionBlock                :: BlockHash -- ^ Block hash
+  , _transactionBlockHeight          :: Integer -- ^ Block number
+  , _transactionSlot                 :: Slot -- ^ Slot number
+  , _transactionIndex                :: Integer -- ^ Transaction index within the block
+  , _transactionOutputAmount         :: [Amount] -- ^ Transaction outputs
+  , _transactionFees                 :: Lovelaces -- ^ Fees of the transaction in Lovelaces
+  , _transactionDeposit              :: Lovelaces -- ^ Deposit within the transaction in Lovelaces
+  , _transactionSize                 :: Integer -- ^ Size of the transaction in Bytes
+  --_ TODO: Text Slots?
+  , _transactionInvalidBefore        :: Maybe Text -- ^ Left (included) endpoint of the timelock validity intervals
+  , _transactionInvalidHereafter     :: Maybe Text -- ^ Right (excluded) endpoint of the timelock validity intervals
+  , _transactionUtxoCount            :: Integer -- ^ Count of UTXOs within the transaction
+  , _transactionWithdrawalCount      :: Integer -- ^ Count of the withdrawals within the transaction
+  , _transactionMirCertCount         :: Integer -- ^  Count of the MIR certificates within the transaction
+  , _transactionDelegationCount      :: Integer -- ^ Count of the delegations within the transaction
+  , _transactionStakeCertCount       :: Integer -- ^ Count of the stake keys (de)registration and delegation certificates within the transaction
+  , _transactionPoolUpdateCount      :: Integer -- ^ Count of the stake pool registration and update certificates within the transaction
+  , _transactionPoolRetireCount      :: Integer -- ^ Count of the stake pool retirement certificates within the transaction
+  , _transactionAssetMintOrBurnCount :: Integer -- ^ Count of asset mints and burns within the transaction
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_transaction", CamelToSnake]] Transaction
+
+instance ToSample Transaction where
+  toSamples = pure $ singleSample
+    Transaction
+      { _transactionHash = "1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477"
+      , _transactionBlock = "356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940"
+      , _transactionBlockHeight = 123456
+      , _transactionSlot = 42000000
+      , _transactionIndex = 1
+      , _transactionOutputAmount = sampleAmounts
+      , _transactionFees = 182485
+      , _transactionDeposit = 0
+      , _transactionSize = 433
+      , _transactionInvalidBefore = Nothing
+      , _transactionInvalidHereafter = Just "13885913"
+      , _transactionUtxoCount = 4
+      , _transactionWithdrawalCount = 0
+      , _transactionMirCertCount =  0
+      , _transactionDelegationCount = 0
+      , _transactionStakeCertCount = 0
+      , _transactionPoolUpdateCount = 0
+      , _transactionPoolRetireCount = 0
+      , _transactionAssetMintOrBurnCount = 0
+      }
+
+-- | Transaction input UTxO
+data UtxoInput = UtxoInput
+  { _utxoInputAddress     :: Address -- ^ Input address
+  , _utxoInputAmount      :: [Amount]
+  , _utxoInputTxHash      :: Text -- ^ Hash of the UTXO transaction
+  , _utxoInputOutputIndex :: Integer -- ^ UTXO index in the transaction
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_utxoInput", CamelToSnake]] UtxoInput
+
+instance ToSample UtxoInput where
+  toSamples = pure $ singleSample utxoInSample
+
+utxoInSample :: UtxoInput
+utxoInSample =
+  UtxoInput
+    { _utxoInputAddress = "addr1q9ld26v2lv8wvrxxmvg90pn8n8n5k6tdst06q2s856rwmvnueldzuuqmnsye359fqrk8hwvenjnqultn7djtrlft7jnq7dy7wv"
+    , _utxoInputAmount = sampleAmounts
+    , _utxoInputTxHash = "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dce628516157f0"
+    , _utxoInputOutputIndex = 0
+    }
+
+-- | Transaction output UTxO
+data UtxoOutput = UtxoOutput
+  { _utxoOutputAddress :: Address -- ^ Output address
+  , _utxoOutputAmount  :: [Amount]
+  } deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_utxoOutput", CamelToSnake]] UtxoOutput
+
+instance ToSample UtxoOutput where
+  toSamples = pure $ singleSample utxoOutSample
+
+utxoOutSample :: UtxoOutput
+utxoOutSample =
+  UtxoOutput
+    { _utxoOutputAddress = "addr1q9ld26v2lv8wvrxxmvg90pn8n8n5k6tdst06q2s856rwmvnueldzuuqmnsye359fqrk8hwvenjnqultn7djtrlft7jnq7dy7wv"
+    , _utxoOutputAmount = sampleAmounts
+    }
+
+-- | Transaction UTxOs
+data TransactionUtxos = TransactionUtxos
+  { _transactionUtxosHash    :: TxHash -- ^ Transaction hash
+  , _transactionUtxosInputs  :: [UtxoInput]
+  , _transactionUtxosOutputs :: [UtxoOutput]
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_transactionUtxos", CamelToSnake]] TransactionUtxos
+
+instance ToSample TransactionUtxos where
+  toSamples = pure $ singleSample
+    TransactionUtxos
+      { _transactionUtxosHash = "1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477"
+      , _transactionUtxosInputs = pure utxoInSample
+      , _transactionUtxosOutputs = pure utxoOutSample
+      }
+
+sampleAmounts :: [Amount]
+sampleAmounts =
+  [ AdaAmount 42000000
+  , AssetAmount
+      $ Money.mkSomeDiscrete
+          "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+          unitScale
+          12
+  ]
+
+-- | Information about (de-)registration of a stake address
+-- within a transaction
+data TransactionStake = TransactionStake
+  { _transactionStakeCertIndex    :: Integer -- ^ Index of the certificate within the transaction
+  , _transactionStakeAddress      :: Address -- ^ Delegation stake address
+  , _transactionStakeRegistration :: Bool -- ^ Registration boolean, false if deregistration
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_transactionStake", CamelToSnake]] TransactionStake
+
+instance ToSample TransactionStake where
+  toSamples = pure $ singleSample
+    TransactionStake
+      { _transactionStakeCertIndex = 0
+      , _transactionStakeAddress = "stake1u9t3a0tcwune5xrnfjg4q7cpvjlgx9lcv0cuqf5mhfjwrvcwrulda"
+      , _transactionStakeRegistration = True
+      }
+
+-- | Information about delegation certificates of a specific transaction
+data TransactionDelegation = TransactionDelegation
+  { _transactionDelegationCertIndex   :: Integer -- ^ Index of the certificate within the transaction
+  , _transactionDelegationAddress     :: Address -- ^ Delegation stake address
+  , _transactionDelegationPoolId      :: PoolId -- ^ Bech32 ID of delegated stake pool
+  , _transactionDelegationActiveEpoch :: Epoch -- ^ Epoch in which the delegation becomes active
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_transactionDelegation", CamelToSnake]] TransactionDelegation
+
+instance ToSample TransactionDelegation where
+  toSamples = pure $ singleSample
+    TransactionDelegation
+      { _transactionDelegationCertIndex = 0
+      , _transactionDelegationAddress = "stake1u9t3a0tcwune5xrnfjg4q7cpvjlgx9lcv0cuqf5mhfjwrvcwrulda"
+      , _transactionDelegationPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      , _transactionDelegationActiveEpoch = 210
+      }
+
+-- | Information about withdrawals of a specific transaction
+data TransactionWithdrawal = TransactionWithdrawal
+  { _transactionWithdrawalAddress :: Address -- ^ Bech32 withdrawal address
+  , _transactionWithdrawalAmount  :: Lovelaces -- ^ Withdrawal amount in Lovelaces
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_transactionWithdrawal", CamelToSnake]] TransactionWithdrawal
+
+instance ToSample TransactionWithdrawal where
+  toSamples = pure $ singleSample
+    TransactionWithdrawal
+      { _transactionWithdrawalAddress = "stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc"
+      , _transactionWithdrawalAmount = 431833601
+      }
+
+-- | Pot from which MIRs are transferred
+data Pot = Reserve | Treasury
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[ConstructorTagModifier '[ToLower]] Pot
+
+instance ToSample Pot where
+  toSamples = pure $ samples [ Reserve, Treasury ]
+
+-- | Information about Move Instantaneous Rewards (MIRs) of a specific transaction
+data TransactionMir = TransactionMir
+  { _transactionMirPot       :: Pot -- ^ Source of MIR funds
+  , _transactionMirCertIndex :: Integer -- ^ Index of the certificate within the transaction
+  , _transactionMirAddress   :: Address -- ^ Bech32 stake address
+  , _transactionMirAmount    :: Lovelaces -- ^ MIR amount in Lovelaces
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_transactionMir", CamelToSnake]] TransactionMir
+
+instance ToSample TransactionMir where
+  toSamples = pure $ singleSample
+    TransactionMir
+      { _transactionMirPot = Reserve
+      , _transactionMirCertIndex = 0
+      , _transactionMirAddress = "stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc"
+      , _transactionMirAmount = 431833601
+      }
+
+-- | Information about stake pool registration and update certificates
+-- of a specific transaction
+data TransactionPoolUpdate = TransactionPoolUpdate
+  { _transactionPoolUpdateCertIndex     :: Integer -- ^ Index of the certificate within the transaction
+  , _transactionPoolUpdatePoolId        :: PoolId -- ^ Bech32 encoded pool ID
+  , _transactionPoolUpdateVrfKey        :: Text -- ^ VRF key hash
+  , _transactionPoolUpdatePledge        :: Lovelaces -- ^ Stake pool certificate pledge in Lovelaces
+  , _transactionPoolUpdateMarginCost    :: Double -- ^  Margin tax cost of the stake pool
+  , _transactionPoolUpdateFixedCost     :: Lovelaces -- ^ Fixed tax cost of the stake pool in Lovelaces
+  , _transactionPoolUpdateRewardAccount :: Address -- ^ Bech32 reward account of the stake pool
+  , _transactionPoolUpdateOwners        :: [Address]
+  , _transactionPoolUpdateMetadata      ::  Maybe PoolUpdateMetadata
+  , _transactionPoolUpdateRelays        :: [PoolRelay]
+  , _transactionPoolUpdateActiveEpoch   :: Epoch -- ^ Epoch that the delegation becomes active
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_transactionPoolUpdate", CamelToSnake]] TransactionPoolUpdate
+
+instance ToSample TransactionPoolUpdate where
+  toSamples = pure $ singleSample
+    TransactionPoolUpdate
+      { _transactionPoolUpdateCertIndex = 0
+      , _transactionPoolUpdatePoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      , _transactionPoolUpdateVrfKey = "0b5245f9934ec2151116fb8ec00f35fd00e0aa3b075c4ed12cce440f999d8233"
+      , _transactionPoolUpdatePledge = 5000000000
+      , _transactionPoolUpdateMarginCost = 0.05
+      , _transactionPoolUpdateFixedCost = 340000000
+      , _transactionPoolUpdateRewardAccount = "stake1uxkptsa4lkr55jleztw43t37vgdn88l6ghclfwuxld2eykgpgvg3f"
+      , _transactionPoolUpdateOwners = [ "stake1u98nnlkvkk23vtvf9273uq7cph5ww6u2yq2389psuqet90sv4xv9v" ]
+      , _transactionPoolUpdateMetadata = Just samplePoolUpdateMetadata
+      , _transactionPoolUpdateRelays = [ samplePoolRelay ]
+      , _transactionPoolUpdateActiveEpoch = 210
+      }
+
+-- | Information about stake pool retirements
+-- within a specific transaction
+data TransactionPoolRetiring = TransactionPoolRetiring
+  { _transactionPoolRetiringCertIndex     :: Integer -- ^ Index of the certificate within the transaction
+  , _transactionPoolRetiringPoolId        :: PoolId -- ^ Bech32 stake pool ID
+  , _transactionPoolRetiringRetiringEpoch :: Epoch -- ^ Retiring epoch
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_transactionPoolRetiring", CamelToSnake]] TransactionPoolRetiring
+
+instance ToSample TransactionPoolRetiring where
+  toSamples = pure $ singleSample
+    TransactionPoolRetiring
+      { _transactionPoolRetiringCertIndex = 0
+      , _transactionPoolRetiringPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      , _transactionPoolRetiringRetiringEpoch = 216
+      }
+
+-- | Transaction metadata in JSON
+data TransactionMetaJSON = TransactionMetaJSON
+  { _transactionMetaJSONLabel        :: Text -- ^ Metadata label
+  , _transactionMetaJSONJSONMetadata :: Maybe Value -- ^ Content of the JSON metadata
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_transactionMetaJSON", CamelToSnake]] TransactionMetaJSON
+
+instance ToSample TransactionMetaJSON where
+  toSamples =
+    let oracleMeta val =
+          object [
+            "ADAUSD" .=
+              [ object [ "value" .= (val :: Text)
+                       , "source" .= ("ergoOracles" :: Text) ]
+              ]
+          ]
+    in pure $ samples
+    [ TransactionMetaJSON
+        "1967"
+        (Just $ object
+           [ "metadata" .= ("https://nut.link/metadata.json" :: Text)
+           , "hash" .= ("6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af" :: Text)
+           ])
+    , TransactionMetaJSON
+        "1968"
+        (Just $ oracleMeta "0.15409850555139935")
+    ]
+
+-- | Transaction metadata in CBOR
+data TransactionMetaCBOR = TransactionMetaCBOR
+  { _transactionMetaCBORLabel        :: Text -- ^ Metadata label
+  , _transactionMetaCBORCBORMetadata :: Maybe Text -- ^ Content of the CBOR metadata
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_transactionMetaCBOR", CamelToSnake]] TransactionMetaCBOR
+
+instance ToSample TransactionMetaCBOR where
+  toSamples = pure $ singleSample $
+    TransactionMetaCBOR
+      "1968"
+      (Just "\\xa100a16b436f6d62696e6174696f6e8601010101010c")
+
+-- | Update of a pool metadata
+data PoolUpdateMetadata = PoolUpdateMetadata
+  { _poolUpdateMetadataUrl         :: Maybe Text -- ^ URL to the stake pool metadata
+  , _poolUpdateMetadataHash        :: Maybe Text -- ^ Hash of the metadata file
+  , _poolUpdateMetadataTicker      :: Maybe Text -- ^ Ticker of the stake pool
+  , _poolUpdateMetadataName        :: Maybe Text -- ^ Name of the stake pool
+  , _poolUpdateMetadataDescription :: Maybe Text -- ^ Description of the stake pool
+  , _poolUpdateMetadataHomepage    :: Maybe Text -- ^ Home page of the stake pool
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_poolUpdateMetadata", CamelToSnake]] PoolUpdateMetadata
+
+-- Note: Similar to PoolMetadata but w/o PoolId and Hex fields
+
+instance ToSample PoolUpdateMetadata where
+  toSamples = pure $ singleSample samplePoolUpdateMetadata
+
+samplePoolUpdateMetadata :: PoolUpdateMetadata
+samplePoolUpdateMetadata =
+  PoolUpdateMetadata
+    { _poolUpdateMetadataUrl = Just "https://stakenuts.com/mainnet.json"
+    , _poolUpdateMetadataHash = Just "47c0c68cb57f4a5b4a87bad896fc274678e7aea98e200fa14a1cb40c0cab1d8c"
+    , _poolUpdateMetadataTicker = Just "NUTS"
+    , _poolUpdateMetadataName = Just "Stake Nuts"
+    , _poolUpdateMetadataDescription = Just "The best pool ever"
+    , _poolUpdateMetadataHomepage = Just "https://stakentus.com/"
+    }
diff --git a/src/Blockfrost/Types/Common.hs b/src/Blockfrost/Types/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Common.hs
@@ -0,0 +1,103 @@
+-- | Types for common servics
+
+module Blockfrost.Types.Common
+  ( URLVersion (..)
+  , Healthy (..)
+  , ServerTime (..)
+  , Metric (..)
+  ) where
+
+import Blockfrost.Types.Shared
+import Data.Aeson
+import Data.Text (Text)
+import Deriving.Aeson
+import Servant.Docs (ToSample (..), samples, singleSample)
+import Test.QuickCheck.Arbitrary (Arbitrary (..))
+import Test.QuickCheck.Instances ()
+import Test.QuickCheck.Modifiers
+
+-- | Root endpoint reply
+data URLVersion = URLVersion
+  { _urlVersionUrl     :: Text
+  , _urlVersionVersion :: Text
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_urlVersion", CamelToSnake]] URLVersion
+
+instance ToSample URLVersion where
+  toSamples = pure $ singleSample $
+    URLVersion "http://blockfrost.io" "0.0.0"
+
+-- | Health endpoint reply
+newtype Healthy = Healthy
+  { isHealthy :: Bool
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[CamelToSnake]] Healthy
+
+instance ToSample Healthy where
+  toSamples = pure $ singleSample $ Healthy False
+
+-- | Health clock endpoint reply
+newtype ServerTime = ServerTime
+  { serverTime :: POSIXTime
+  }
+  deriving stock (Show, Eq, Generic)
+
+instance FromJSON ServerTime where
+  parseJSON = withObject "ServerTime"
+    $ \v -> v .: "server_time"
+    >>= \t -> ServerTime . millisecondsToPosix <$> parseJSON @Integer t
+
+instance ToJSON ServerTime where
+  toJSON (ServerTime t) =
+    object [
+      "server_time" .= posixToMilliseconds t
+    ]
+
+instance ToSample ServerTime where
+    toSamples _ = singleSample $ ServerTime $ millisecondsToPosix 1603400958947
+
+instance Arbitrary ServerTime where
+  arbitrary = do
+    Positive (n :: Integer) <- arbitrary
+    pure $ ServerTime (fromInteger n / 1000)
+
+-- | Metrics response
+data Metric = Metric {
+    _metricTime  :: POSIXTime
+  , _metricCalls :: Integer
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_metric", CamelToSnake]] Metric
+
+instance ToSample Metric where
+    toSamples _ = samples $
+      [ Metric 1612543884 42
+      , Metric 1614523884 6942
+      ]
+
+-- Re-use @Metric@ for response with endpoint field
+instance {-# OVERLAPS #-} ToJSON (Text, Metric) where
+  toJSON (enp, m) = case toJSON m of
+    (Object o) -> Object (o <> ("endpoint" .= (toJSON enp)))
+    _          -> error "Absurd"
+
+instance {-# OVERLAPS #-} FromJSON (Text, Metric) where
+  parseJSON v@(Object o) = do
+    enp <- o .: "endpoint"
+    ticker <- parseJSON v
+    return (enp, ticker)
+  parseJSON _ = fail "Unexpected type for (Text, Metric)"
+
+instance {-# OVERLAPS #-} ToSample (Text, Metric) where
+  toSamples _ = samples $
+    [ ("block", Metric 1612543814 182)
+    , ("epoch", Metric 1612543814 42)
+    , ("block", Metric 1612543812 775)
+    , ("epoch", Metric 1612523884 4)
+    , ("block", Metric 1612553884 89794)
+    ]
diff --git a/src/Blockfrost/Types/IPFS.hs b/src/Blockfrost/Types/IPFS.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/IPFS.hs
@@ -0,0 +1,102 @@
+-- | Types for IPFS servics
+
+module Blockfrost.Types.IPFS
+  ( IPFSAdd (..)
+  , IPFSPinChange (..)
+  , IPFSPin (..)
+  , PinState (..)
+  , IPFSData (..)
+  ) where
+
+import Blockfrost.Types.Shared
+import Data.Aeson
+import Data.ByteString.Lazy (ByteString)
+import Data.Text (Text)
+import Deriving.Aeson
+import Servant.API
+import Servant.Docs (ToSample (..), singleSample)
+
+-- | IPFS Add response
+data IPFSAdd = IPFSAdd
+  { _ipfsAddName     :: Text -- ^ Name of the file
+  , _ipfsAddIpfsHash :: Text -- ^ IPFS hash of the file
+  , _ipfsAddSize     :: Quantity -- ^ Size of the IPFS node
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_ipfsAdd", CamelToSnake]] IPFSAdd
+
+instance ToSample IPFSAdd where
+  toSamples = pure $ singleSample $
+    IPFSAdd
+      "README.md"
+      "QmZbHqiCxKEVX7QfijzJTkZiSi3WEVTcvANgNAWzDYgZDr"
+      125297
+
+-- | State of the pinned object,
+-- which is @Queued@ when we are retriving object.
+--
+-- If this is successful the state is changed to @Pinned@ or @Failed@ if not.
+-- The state @Gc@ means the pinned item has been garbage collected
+-- due to account being over storage quota or after it has been
+-- moved to @Unpinned@ state by removing the object pin.
+data PinState = Queued | Pinned | Unpinned | Failed | Gc
+  deriving (Eq, Show, Ord, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[ConstructorTagModifier '[ToLower]] PinState
+
+
+-- | IPFS Pin Add response
+data IPFSPinChange = IPFSPinChange
+  { _ipfsPinChangeIpfsHash :: Text -- ^ IPFS hash of pinned object
+  , _ipfsPinChangeState    :: PinState -- ^ State of the pin action
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_ipfsPinChange", CamelToSnake]] IPFSPinChange
+
+instance ToSample IPFSPinChange where
+  toSamples = pure $ singleSample $
+    IPFSPinChange
+      "QmPojRfAXYAXV92Dof7gtSgaVuxEk64xx9CKvprqu9VwA8"
+      Queued
+
+-- | IPFS Pin information
+data IPFSPin = IPFSPin
+  { _ipfsPinTimeCreated :: POSIXTime -- ^ Creation time of the IPFS object on our backends
+  , _ipfsPinTimePinned  :: POSIXTime -- ^ Pin time of the IPFS object on our backends
+  , _ipfsPinIpfsHash    :: Text -- ^ IPFS hash of the pinned object
+  , _ipfsPinSize        :: Quantity -- ^ Size of the IPFS node
+  , _ipfsPinState       :: PinState -- ^ State of the pinned object
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_ipfsPin", CamelToSnake]] IPFSPin
+
+instance ToSample IPFSPin where
+  toSamples = pure $ singleSample $
+    IPFSPin
+      { _ipfsPinTimeCreated = 1615551024
+      , _ipfsPinTimePinned = 1615551024
+      , _ipfsPinIpfsHash = "QmdVMnULrY95mth2XkwjxDtMHvzuzmvUPTotKE1tgqKbCx"
+      , _ipfsPinSize = 1615551024
+      , _ipfsPinState = Pinned
+      }
+
+newtype IPFSData = IPFSData ByteString
+  deriving (Show, Eq, Generic)
+
+instance ToSample IPFSData where
+  toSamples = pure $ singleSample $ IPFSData "sample ipfs bytestring"
+
+instance MimeRender OctetStream IPFSData where
+  mimeRender _ (IPFSData cs) = cs
+
+instance MimeUnrender OctetStream IPFSData where
+  mimeUnrender _ lbs = pure $ IPFSData lbs
+
+instance MimeRender PlainText IPFSData where
+  mimeRender _ (IPFSData cs) = cs
+
+instance MimeUnrender PlainText IPFSData where
+  mimeUnrender _ lbs = pure $ IPFSData lbs
diff --git a/src/Blockfrost/Types/NutLink.hs b/src/Blockfrost/Types/NutLink.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/NutLink.hs
@@ -0,0 +1,95 @@
+-- | Types for Nut.link servics
+
+module Blockfrost.Types.NutLink
+  ( NutlinkAddress (..)
+  , NutlinkAddressTicker (..)
+  , NutlinkTicker (..)
+  ) where
+
+import Blockfrost.Types.Shared
+import Data.Aeson
+import Data.Text (Text)
+import Deriving.Aeson
+import Servant.Docs (ToSample (..), samples, singleSample)
+
+-- | Specific address metadata
+data NutlinkAddress = NutlinkAddress
+  { _nutlinkAddressAddress      :: Address -- ^ Bech32 encoded address
+  , _nutlinkAddressMetadataUrl  :: Text -- ^ URL of the specific metadata file
+  , _nutlinkAddressMetadataHash :: Text -- ^ Hash of the metadata file
+  , _nutlinkAddressMetadata     :: Maybe Value -- ^ The cached metadata of the metadata_url file.
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_nutlinkAddress", CamelToSnake]] NutlinkAddress
+
+instance ToSample NutlinkAddress where
+  toSamples = pure $ singleSample $
+    NutlinkAddress
+      { _nutlinkAddressAddress = "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz"
+      , _nutlinkAddressMetadataUrl = "https://nut.link/metadata.json"
+      , _nutlinkAddressMetadataHash = "6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af"
+      , _nutlinkAddressMetadata = pure $ object []
+      }
+
+-- | Ticker for specific metadata oracle
+data NutlinkAddressTicker = NutlinkAddressTicker
+  { _nutlinkAddressTickerName        :: Text -- ^ Name of the ticker
+  , _nutlinkAddressTickerCount       :: Integer -- ^ Number of ticker records
+  , _nutlinkAddressTickerLatestBlock :: Integer -- ^ Block height of the latest record
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_nutlinkAddressTicker", CamelToSnake]] NutlinkAddressTicker
+
+instance ToSample NutlinkAddressTicker where
+  toSamples = pure $ samples $
+    [ NutlinkAddressTicker
+        { _nutlinkAddressTickerName = "ADAUSD"
+        , _nutlinkAddressTickerCount = 1980038
+        , _nutlinkAddressTickerLatestBlock = 2657092
+        }
+    , NutlinkAddressTicker
+        { _nutlinkAddressTickerName = "ADAEUR"
+        , _nutlinkAddressTickerCount = 1980038
+        , _nutlinkAddressTickerLatestBlock = 2657092
+        }
+    , NutlinkAddressTicker
+        { _nutlinkAddressTickerName = "ADABTC"
+        , _nutlinkAddressTickerCount = 1980038
+        , _nutlinkAddressTickerLatestBlock = 2657092
+        }
+    ]
+
+-- | Specific ticker record
+data NutlinkTicker = NutlinkTicker
+  { _nutlinkTickerTxHash      :: TxHash -- ^ Hash of the transaction
+  , _nutlinkTickerBlockHeight :: Integer -- ^ Block height of the record
+  , _nutlinkTickerTxIndex     :: Integer -- ^ Transaction index within the block
+  , _nutlinkTickerPayload     :: Value -- ^ Content of the ticker
+  }
+  deriving stock (Show, Eq, Generic)
+  deriving (FromJSON, ToJSON)
+  via CustomJSON '[FieldLabelModifier '[StripPrefix "_nutlinkTicker", CamelToSnake]] NutlinkTicker
+
+instance ToSample NutlinkTicker where
+  toSamples = pure $ singleSample $
+    NutlinkTicker
+      { _nutlinkTickerTxHash = "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b"
+      , _nutlinkTickerBlockHeight = 2657092
+      , _nutlinkTickerTxIndex = 8
+      , _nutlinkTickerPayload = object []
+      }
+
+-- Re-use @NutlinkTicker@ for response with address field
+instance {-# OVERLAPS #-} ToJSON (Address, NutlinkTicker) where
+  toJSON (addr, nt) = case toJSON nt of
+    (Object o) -> Object (o <> ("address" .= (toJSON addr)))
+    _          -> error "Absurd"
+
+instance {-# OVERLAPS #-} FromJSON (Address, NutlinkTicker) where
+  parseJSON v@(Object o) = do
+    addr <- o .: "address"
+    ticker <- parseJSON v
+    return (addr, ticker)
+  parseJSON _ = fail "Unexpected type for (Address, NutlinkTicker)"
diff --git a/src/Blockfrost/Types/Shared.hs b/src/Blockfrost/Types/Shared.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared.hs
@@ -0,0 +1,35 @@
+-- | Shared types and utilities
+
+module Blockfrost.Types.Shared
+  ( module Blockfrost.Types.Shared.Ada
+  , module Blockfrost.Types.Shared.Address
+  , module Blockfrost.Types.Shared.Amount
+  , module Blockfrost.Types.Shared.AssetId
+  , module Blockfrost.Types.Shared.BlockHash
+  , module Blockfrost.Types.Shared.BlockIndex
+  , module Blockfrost.Types.Shared.CBOR
+  , module Blockfrost.Types.Shared.Epoch
+  , module Blockfrost.Types.Shared.Opts
+  , module Blockfrost.Types.Shared.POSIXMillis
+  , module Blockfrost.Types.Shared.PolicyId
+  , module Blockfrost.Types.Shared.PoolId
+  , module Blockfrost.Types.Shared.Quantity
+  , module Blockfrost.Types.Shared.Slot
+  , module Blockfrost.Types.Shared.TxHash
+  ) where
+
+import Blockfrost.Types.Shared.Ada
+import Blockfrost.Types.Shared.Address
+import Blockfrost.Types.Shared.Amount
+import Blockfrost.Types.Shared.AssetId
+import Blockfrost.Types.Shared.BlockHash
+import Blockfrost.Types.Shared.BlockIndex
+import Blockfrost.Types.Shared.CBOR
+import Blockfrost.Types.Shared.Epoch
+import Blockfrost.Types.Shared.Opts
+import Blockfrost.Types.Shared.POSIXMillis
+import Blockfrost.Types.Shared.PolicyId
+import Blockfrost.Types.Shared.PoolId
+import Blockfrost.Types.Shared.Quantity
+import Blockfrost.Types.Shared.Slot
+import Blockfrost.Types.Shared.TxHash
diff --git a/src/Blockfrost/Types/Shared/Ada.hs b/src/Blockfrost/Types/Shared/Ada.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/Ada.hs
@@ -0,0 +1,55 @@
+-- | Lovelaces
+
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Blockfrost.Types.Shared.Ada
+  ( Lovelaces
+  ) where
+
+import Data.Aeson (Encoding, FromJSON (..), ToJSON (..), Value)
+import Data.Proxy (Proxy (..))
+import Data.Text (Text)
+import GHC.TypeLits (KnownSymbol)
+import qualified Money
+import Servant.Docs (ToSample (..), samples)
+
+type Lovelaces = Money.Discrete "ADA" "lovelace"
+
+instance (KnownSymbol cur, Money.GoodScale sc) => ToSample (Money.Discrete' cur sc) where
+  toSamples _ = samples $ map Money.discrete [
+      592661
+    , 3592661
+    , 1337
+    , 42
+    , 0
+    , 3000100
+    ]
+
+lovelaceDecimalConfig :: Money.DecimalConf
+lovelaceDecimalConfig =
+  Money.defaultDecimalConf
+    { Money.decimalConf_digits = 0
+    , Money.decimalConf_scale =
+        Money.scale (Proxy @(Money.UnitScale "ADA" "lovelace"))
+    }
+
+instance ToJSON (Money.Discrete' "ADA" '(1_000_000, 1)) where
+  toJSON =
+      (toJSON :: Text -> Value)
+    . Money.discreteToDecimal
+        lovelaceDecimalConfig
+        Money.Round
+  toEncoding =
+      (toEncoding :: Text -> Encoding)
+    . Money.discreteToDecimal
+        lovelaceDecimalConfig
+        Money.Round
+
+instance FromJSON (Money.Discrete' "ADA" '(1000000, 1)) where
+  parseJSON v = do
+     t <- parseJSON v
+     maybe
+        (fail "Can't parse Discrete ADA value")
+        pure
+        (Money.discreteFromDecimal lovelaceDecimalConfig t)
diff --git a/src/Blockfrost/Types/Shared/Address.hs b/src/Blockfrost/Types/Shared/Address.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/Address.hs
@@ -0,0 +1,37 @@
+-- | Address newtype
+
+module Blockfrost.Types.Shared.Address
+  where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.String (IsString (..))
+import Data.Text (Text)
+import qualified Data.Text
+import GHC.Generics
+import Servant.API (Capture, FromHttpApiData (..), ToHttpApiData (..))
+import Servant.Docs (DocCapture (..), ToCapture (..), ToSample (..), samples)
+
+newtype Address = Address Text
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (FromHttpApiData, ToHttpApiData, FromJSON, ToJSON)
+
+mkAddress :: Text -> Address
+mkAddress = Address
+
+unAddress :: Address -> Text
+unAddress (Address a) = a
+
+instance IsString Address where
+  fromString = mkAddress . Data.Text.pack
+
+instance ToCapture (Capture "address" Address) where
+  toCapture _ = DocCapture "address" "Bech32 encoded address"
+
+instance ToCapture (Capture "stake_address" Address) where
+  toCapture _ = DocCapture "stake_address" "Bech32 stake address"
+
+instance ToSample Address where
+    toSamples = pure $ samples [
+        "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz"
+      , "stake1ux3g2c9dx2nhhehyrezyxpkstartcqmu9hk63qgfkccw5rqttygt7"
+      ]
diff --git a/src/Blockfrost/Types/Shared/Amount.hs b/src/Blockfrost/Types/Shared/Amount.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/Amount.hs
@@ -0,0 +1,79 @@
+-- | Amount sum type
+
+{-# LANGUAGE NumericUnderscores #-}
+
+module Blockfrost.Types.Shared.Amount
+  where
+
+import Blockfrost.Types.Shared.Ada (Lovelaces)
+import Data.Aeson
+  ( FromJSON (..)
+  , ToJSON (..)
+  , Value (Object)
+  , object
+  , withObject
+  , (.:)
+  , (.=)
+  )
+import GHC.Generics
+import qualified Money
+import Servant.Docs (ToSample (..), samples)
+import qualified Text.Read
+
+-- | Amount, which is either `AdaAmount Lovelaces` representing
+-- amount of lovelaces or `AssetAmount SomeDiscrete` for asset amounts,
+-- identified by concatenation of asset policy ID
+-- and hex-encoded asset_name
+data Amount =
+    AdaAmount Lovelaces
+  | AssetAmount Money.SomeDiscrete
+  deriving (Eq, Show, Ord, Generic)
+
+-- | SomeDiscrete values always use scale of 1
+unitScale :: Money.Scale
+unitScale = let (Just s) = Money.scaleFromRational 1 in s
+
+instance ToJSON Money.SomeDiscrete where
+  toJSON sd =
+    object [ "unit" .= Money.someDiscreteCurrency sd
+           , "quantity" .= show (Money.someDiscreteAmount sd)
+           ]
+
+instance FromJSON Money.SomeDiscrete where
+  parseJSON = withObject "amount" $ \o -> do
+    u <- o .: "unit"
+    (strQuant :: String) <- o .: "quantity"
+    case Text.Read.readMaybe strQuant of
+      Nothing    -> fail "Unable to read quantity as Integer"
+      Just quant -> pure $ Money.mkSomeDiscrete u unitScale quant
+
+instance ToJSON Amount where
+  toJSON (AdaAmount lovelaces) =
+    object [ "unit" .= ("lovelace" :: String)
+           , "quantity" .= toJSON lovelaces
+           ]
+  toJSON (AssetAmount av) = toJSON av
+
+instance FromJSON Amount where
+  parseJSON x@(Object o) = do
+    (u :: String) <- o .: "unit"
+    v <- o .: "quantity"
+    case u of
+      "lovelace" -> AdaAmount <$> parseJSON v
+      _          -> AssetAmount <$> parseJSON x
+  parseJSON other = fail $ "Amount expecting object, got" ++ show other
+
+instance ToSample Amount where
+  toSamples = pure $ samples
+    [ AdaAmount 42000000
+    , AssetAmount
+        $ Money.toSomeDiscrete
+          (12 :: Money.Discrete'
+                    "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+                    '(1,1))
+    , AssetAmount
+        $ Money.toSomeDiscrete
+          (18605647 :: Money.Discrete'
+                          "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad"
+                          '(1,1))
+    ]
diff --git a/src/Blockfrost/Types/Shared/AssetId.hs b/src/Blockfrost/Types/Shared/AssetId.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/AssetId.hs
@@ -0,0 +1,36 @@
+-- |  AssetIds
+
+module Blockfrost.Types.Shared.AssetId
+  where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.String (IsString (..))
+import Data.Text (Text)
+import qualified Data.Text
+import GHC.Generics
+import Servant.API (Capture, FromHttpApiData (..), ToHttpApiData (..))
+import Servant.Docs (DocCapture (..), ToCapture (..), ToSample (..), samples)
+
+-- | Concatenation of asset policy ID
+-- and hex-encoded asset name
+newtype AssetId = AssetId Text
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (FromHttpApiData, ToHttpApiData, FromJSON, ToJSON)
+
+mkAssetId :: Text -> AssetId
+mkAssetId = AssetId
+
+unAssetId :: AssetId -> Text
+unAssetId (AssetId a) = a
+
+instance IsString AssetId where
+  fromString = mkAssetId . Data.Text.pack
+
+instance ToCapture (Capture "asset" AssetId) where
+  toCapture _ = DocCapture "asset" "Concatenation of the policy_id and hex-encoded asset_name"
+
+instance ToSample AssetId where
+    toSamples = pure $ samples [
+        "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+      , "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad"
+      ]
diff --git a/src/Blockfrost/Types/Shared/BlockHash.hs b/src/Blockfrost/Types/Shared/BlockHash.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/BlockHash.hs
@@ -0,0 +1,50 @@
+-- | Hash of the block
+
+module Blockfrost.Types.Shared.BlockHash
+  where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.Char (isDigit)
+import Data.String (IsString (..))
+import Data.Text (Text)
+import qualified Data.Text
+import GHC.Generics
+import Servant.API (Capture, FromHttpApiData (..), ToHttpApiData (..))
+import Servant.Docs (DocCapture (..), ToCapture (..), ToSample (..), samples)
+import qualified Text.Read
+
+newtype BlockHash = BlockHash Text
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (FromHttpApiData, ToHttpApiData, FromJSON, ToJSON)
+
+mkBlockHash :: Text -> BlockHash
+mkBlockHash = BlockHash
+
+unBlockHash :: BlockHash -> Text
+unBlockHash (BlockHash a) = a
+
+instance IsString BlockHash where
+  fromString = mkBlockHash . Data.Text.pack
+
+instance ToCapture (Capture "block_hash" BlockHash) where
+  toCapture _ = DocCapture "block_hash" "Specific block hash"
+
+instance ToSample BlockHash where
+    toSamples = pure $ samples
+      [ "d0fa315687e99ccdc96b14cc2ea74a767405d64427b648c470731a9b69e4606e"
+      , "38bc6efb92a830a0ed22a64f979d120d26483fd3c811f6622a8c62175f530878"
+      , "f3258fcd8b975c061b4fcdcfcbb438807134d6961ec278c200151274893b6b7d"
+      ]
+
+instance ToCapture (Capture "hash_or_number" (Either Integer BlockHash)) where
+  toCapture _ = DocCapture "hash_or_number" "Hash or number of the requested block."
+
+instance {-# OVERLAPS #-} ToHttpApiData (Either Integer BlockHash) where
+  toUrlPiece = either (Data.Text.pack . show) unBlockHash
+
+instance {-# OVERLAPS #-} FromHttpApiData (Either Integer BlockHash) where
+  parseUrlPiece x | Data.Text.all isDigit x =
+    case Text.Read.readMaybe (Data.Text.unpack x) of
+        Nothing      -> Left "Unable to read block id"
+        Just blockId -> pure (Left blockId)
+  parseUrlPiece x | otherwise = pure (Right (BlockHash x))
diff --git a/src/Blockfrost/Types/Shared/BlockIndex.hs b/src/Blockfrost/Types/Shared/BlockIndex.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/BlockIndex.hs
@@ -0,0 +1,60 @@
+-- | BlockIndex query parameter
+
+module Blockfrost.Types.Shared.BlockIndex
+  where
+
+import qualified Data.Text
+import GHC.Generics
+import Servant.API (FromHttpApiData (..), QueryParam, ToHttpApiData (..))
+import Servant.Docs
+  ( DocQueryParam (..)
+  , ParamKind (..)
+  , ToParam (..)
+  , ToSample (..)
+  , samples
+  )
+
+-- | Block height (number) and optional index
+data BlockIndex = BlockIndex {
+    blockIndexHeight :: Integer
+  , blockIndexIndex  :: Maybe Integer
+  }
+  deriving stock (Eq, Show, Generic)
+
+instance ToHttpApiData BlockIndex where
+  toUrlPiece bi = Data.Text.pack $
+       show (blockIndexHeight bi)
+    <> maybe mempty ((':':) .show) (blockIndexIndex bi)
+
+instance FromHttpApiData BlockIndex where
+  parseUrlPiece x = case Data.Text.splitOn ":" x of
+    [""] -> Left "Empty block index"
+    [bh] -> (`BlockIndex` Nothing) <$> parseUrlPiece bh
+    [bh, idx] -> BlockIndex <$> parseUrlPiece bh <*> (Just <$> parseUrlPiece idx)
+    _ -> Left "Invalid block index"
+
+instance ToParam (QueryParam "from" BlockIndex) where
+  toParam _ =
+    DocQueryParam
+      "blockIndex"
+      []
+      "The block number and optionally also index from which (inclusive)\
+      \ to start search for results, concatenated using colon.\
+      \ Has to be lower than or equal to `to` parameter."
+      Normal
+
+instance ToParam (QueryParam "to" BlockIndex) where
+  toParam _ =
+    DocQueryParam
+      "blockIndex"
+      []
+      "The block number and optionally also index from which (inclusive)\
+      \ to end the search for results, concatenated using colon.\
+      \ Has to be higher than or equal to `from` parameter."
+      Normal
+
+instance ToSample BlockIndex where
+    toSamples = pure $ samples [
+        BlockIndex 892961 Nothing
+      , BlockIndex 9999269 (Just 10)
+      ]
diff --git a/src/Blockfrost/Types/Shared/CBOR.hs b/src/Blockfrost/Types/Shared/CBOR.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/CBOR.hs
@@ -0,0 +1,27 @@
+-- | CBOR Servant support and wrapper type
+
+module Blockfrost.Types.Shared.CBOR
+  where
+
+import Data.ByteString.Lazy (ByteString)
+import Servant.API (Accept (..), MimeRender (..), MimeUnrender (..))
+import Servant.Docs (ToSample (..), singleSample)
+
+data CBOR
+
+-- | Wrapper for CBOR encoded `ByteString`s
+-- used for submitting a transaction
+newtype CBORString = CBORString ByteString
+  deriving stock (Eq, Show)
+
+instance Accept CBOR where
+  contentType = pure "application/cbor"
+
+instance MimeRender CBOR CBORString where
+  mimeRender _ (CBORString cs) = cs
+
+instance MimeUnrender CBOR CBORString where
+  mimeUnrender _ lbs = pure $ CBORString lbs
+
+instance ToSample CBORString where
+  toSamples = pure $ singleSample $ CBORString "adef"
diff --git a/src/Blockfrost/Types/Shared/Epoch.hs b/src/Blockfrost/Types/Shared/Epoch.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/Epoch.hs
@@ -0,0 +1,22 @@
+-- | Epoch
+
+module Blockfrost.Types.Shared.Epoch
+  where
+
+import Data.Aeson (FromJSON, ToJSON)
+import GHC.Generics
+import Servant.API (Capture, FromHttpApiData (..), ToHttpApiData (..))
+import Servant.Docs (DocCapture (..), ToCapture (..), ToSample (..), samples)
+
+newtype Epoch = Epoch Integer
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (Num, FromHttpApiData, ToHttpApiData, FromJSON, ToJSON)
+
+unEpoch :: Epoch -> Integer
+unEpoch (Epoch i) = i
+
+instance ToCapture (Capture "epoch_number" Epoch) where
+  toCapture _ = DocCapture "epoch_number" "Epoch for specific epoch slot."
+
+instance ToSample Epoch where
+    toSamples = pure $ samples [425, 500, 1200]
diff --git a/src/Blockfrost/Types/Shared/Opts.hs b/src/Blockfrost/Types/Shared/Opts.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/Opts.hs
@@ -0,0 +1,25 @@
+-- | Aeson options and helpers
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.Types.Shared.Opts
+  ( ToLower
+  , aesonOptions
+  ) where
+
+import Data.Aeson (Options (..), camelTo2, defaultOptions)
+import Data.Char (toLower)
+import Deriving.Aeson (StringModifier (..))
+
+data ToLower
+
+instance StringModifier ToLower where
+  getStringModifier ""       = ""
+  getStringModifier (c : xs) = toLower c : xs
+
+aesonOptions :: Maybe String -> Options
+aesonOptions mPrefix = defaultOptions {
+   fieldLabelModifier = camelTo2 '_'  . dropIfPrefixed
+ , constructorTagModifier = map toLower
+ }
+ where dropIfPrefixed = maybe id (drop . length) mPrefix
diff --git a/src/Blockfrost/Types/Shared/POSIXMillis.hs b/src/Blockfrost/Types/Shared/POSIXMillis.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/POSIXMillis.hs
@@ -0,0 +1,48 @@
+-- | POSIX Milliseconds wrapper
+
+module Blockfrost.Types.Shared.POSIXMillis
+  ( POSIXMillis
+  , POSIXTime
+  , millisecondsToPosix
+  , posixToMilliseconds
+  , seconds
+  ) where
+
+import Data.Aeson (FromJSON (..), ToJSON (..))
+import Data.Time.Clock.POSIX
+import GHC.Generics
+import Servant.Docs (ToSample (..), singleSample)
+import Test.QuickCheck.Arbitrary
+import Test.QuickCheck.Instances ()
+import Test.QuickCheck.Modifiers
+
+-- | Convert `Integer` milliseconds to `POSIXTime`
+millisecondsToPosix :: Integer -> POSIXTime
+millisecondsToPosix n = fromInteger n / 1000
+
+-- | Convert `POSIXTime` to `Integer` milliseconds
+posixToMilliseconds :: POSIXTime -> Integer
+posixToMilliseconds t = round (t * 1000)
+
+newtype POSIXMillis = POSIXMillis {
+    unPOSIXMillis :: POSIXTime
+  }
+  deriving stock (Show, Eq, Generic)
+
+seconds :: POSIXTime -> POSIXMillis
+seconds = POSIXMillis
+
+instance FromJSON POSIXMillis where
+  parseJSON t = POSIXMillis . millisecondsToPosix <$> parseJSON @Integer t
+
+instance ToJSON POSIXMillis where
+  toJSON (POSIXMillis t) = toJSON $ posixToMilliseconds t
+  toEncoding (POSIXMillis t) = toEncoding $ posixToMilliseconds t
+
+instance ToSample POSIXMillis where
+    toSamples _ = singleSample $ POSIXMillis $ millisecondsToPosix 1603400958947
+
+instance Arbitrary POSIXMillis where
+  arbitrary = do
+    Positive (n :: Integer) <- arbitrary
+    pure $ POSIXMillis (fromInteger n / 1000)
diff --git a/src/Blockfrost/Types/Shared/PolicyId.hs b/src/Blockfrost/Types/Shared/PolicyId.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/PolicyId.hs
@@ -0,0 +1,34 @@
+-- | PolicyId
+
+module Blockfrost.Types.Shared.PolicyId
+  where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.String (IsString (..))
+import Data.Text (Text)
+import qualified Data.Text
+import GHC.Generics
+import Servant.API (Capture, FromHttpApiData (..), ToHttpApiData (..))
+import Servant.Docs (DocCapture (..), ToCapture (..), ToSample (..), samples)
+
+-- | Minting policy Id
+newtype PolicyId = PolicyId Text
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (FromHttpApiData, ToHttpApiData, FromJSON, ToJSON)
+
+mkPolicyId :: Text -> PolicyId
+mkPolicyId = PolicyId
+
+unPolicyId :: PolicyId -> Text
+unPolicyId (PolicyId a) = a
+
+instance IsString PolicyId where
+  fromString = mkPolicyId . Data.Text.pack
+
+instance ToCapture (Capture "policy_id" PolicyId) where
+  toCapture _ = DocCapture "policy_id" "Specific policy_id"
+
+instance ToSample PolicyId where
+    toSamples = pure $ samples [
+        "476039a0949cf0b22f6a800f56780184c44533887ca6e821007840c3"
+      ]
diff --git a/src/Blockfrost/Types/Shared/PoolId.hs b/src/Blockfrost/Types/Shared/PoolId.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/PoolId.hs
@@ -0,0 +1,33 @@
+-- | Pool identifier
+
+module Blockfrost.Types.Shared.PoolId
+  where
+
+import Data.Aeson (FromJSON, ToJSON)
+import Data.String (IsString (..))
+import Data.Text (Text)
+import qualified Data.Text
+import GHC.Generics
+import Servant.API (Capture, FromHttpApiData (..), ToHttpApiData (..))
+import Servant.Docs (DocCapture (..), ToCapture (..), ToSample (..), samples)
+
+newtype PoolId = PoolId Text
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (FromHttpApiData, ToHttpApiData, FromJSON, ToJSON)
+
+mkPoolId :: Text -> PoolId
+mkPoolId = PoolId
+
+unPoolId :: PoolId -> Text
+unPoolId (PoolId a) = a
+
+instance IsString PoolId where
+  fromString = mkPoolId . Data.Text.pack
+
+instance ToCapture (Capture "pool_id" PoolId) where
+  toCapture _ = DocCapture "pool_id" "Specific pool_id"
+
+instance ToSample PoolId where
+    toSamples = pure $ samples [
+        "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      ]
diff --git a/src/Blockfrost/Types/Shared/Quantity.hs b/src/Blockfrost/Types/Shared/Quantity.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/Quantity.hs
@@ -0,0 +1,31 @@
+-- | Quantity wrapper
+
+module Blockfrost.Types.Shared.Quantity
+  where
+
+import Data.Aeson (FromJSON (..), ToJSON (..), withText)
+import qualified Data.Text
+import GHC.Generics
+import Servant.API (FromHttpApiData (..), ToHttpApiData (..))
+import Servant.Docs (ToSample (..), samples)
+import qualified Text.Read
+
+newtype Quantity = Quantity Integer
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (Num, Read, FromHttpApiData, ToHttpApiData)
+
+unQuantity :: Quantity -> Integer
+unQuantity (Quantity i) = i
+
+instance ToJSON Quantity where
+  toJSON = toJSON . show . unQuantity
+  toEncoding = toEncoding . show . unQuantity
+
+instance FromJSON Quantity where
+  parseJSON = withText "quantity" $ \q -> do
+    case Text.Read.readMaybe (Data.Text.unpack q) of
+      Nothing    -> fail "Unable to read quantity as Integer"
+      Just quant -> pure quant
+
+instance ToSample Quantity where
+    toSamples = pure $ samples [37040682, 412162133]
diff --git a/src/Blockfrost/Types/Shared/Slot.hs b/src/Blockfrost/Types/Shared/Slot.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/Slot.hs
@@ -0,0 +1,22 @@
+-- | Slot wrapper
+
+module Blockfrost.Types.Shared.Slot
+  where
+
+import Data.Aeson (FromJSON, ToJSON)
+import GHC.Generics
+import Servant.API (Capture, FromHttpApiData (..), ToHttpApiData (..))
+import Servant.Docs (DocCapture (..), ToCapture (..), ToSample (..), samples)
+
+newtype Slot = Slot Integer
+  deriving stock (Eq, Show, Generic)
+  deriving newtype (Num, FromHttpApiData, ToHttpApiData, FromJSON, ToJSON)
+
+unSlot :: Slot -> Integer
+unSlot (Slot i) = i
+
+instance ToCapture (Capture "slot_number" Slot) where
+  toCapture _ = DocCapture "slot_number" "Slot position for requested block."
+
+instance ToSample Slot where
+    toSamples = pure $ samples [37040682, 412162133]
diff --git a/src/Blockfrost/Types/Shared/TxHash.hs b/src/Blockfrost/Types/Shared/TxHash.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Types/Shared/TxHash.hs
@@ -0,0 +1,37 @@
+-- | Transaction Id
+
+module Blockfrost.Types.Shared.TxHash
+  ( TxHash (..)
+  ) where
+
+import Data.Aeson (FromJSON (..), ToJSON (..))
+import Data.String (IsString (..))
+import Data.Text (Text)
+import qualified Data.Text
+import GHC.Generics
+import Servant.API (Capture, FromHttpApiData (..), ToHttpApiData (..))
+import Servant.Docs (DocCapture (..), ToCapture (..), ToSample (..), samples)
+
+-- | Id (hash) of the transaction
+newtype TxHash = TxHash { unTxHash :: Text }
+  deriving stock (Show, Eq, Generic)
+  deriving newtype (FromHttpApiData, ToHttpApiData)
+
+instance IsString TxHash where
+  fromString = TxHash . Data.Text.pack
+
+instance ToJSON TxHash where
+  toJSON = toJSON . unTxHash
+  toEncoding = toEncoding . unTxHash
+instance FromJSON TxHash where
+  parseJSON = fmap TxHash <$> parseJSON
+
+instance ToSample TxHash where
+    toSamples _ = samples $ map TxHash
+      [ "8788591983aa73981fc92d6cddbbe643959f5a784e84b8bee0db15823f575a5b"
+      , "52e748c4dec58b687b90b0b40d383b9fe1f24c1a833b7395cdf07dd67859f46f"
+      , "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b"
+      ]
+
+instance ToCapture (Capture "hash" TxHash) where
+  toCapture _ = DocCapture "hash" "Hash of the requested transaction."
diff --git a/src/Blockfrost/Util/LensRules.hs b/src/Blockfrost/Util/LensRules.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Util/LensRules.hs
@@ -0,0 +1,35 @@
+-- | Rules for lens generation
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.Util.LensRules
+  where
+
+import Control.Lens
+import Language.Haskell.TH (mkName, nameBase)
+
+-- Bit of an overkill, since we only alter
+-- one field name which would end up called `type`
+blockfrostFieldRules :: LensRules
+blockfrostFieldRules = defaultFieldRules
+  & lensField %~ modNamer
+  where
+    modNamer namer dname fnames fname =
+      map fixDefName (namer (fixTypeName dname) fnames fname)
+
+    fixDefName (MethodName cname mname)=MethodName cname (fixName mname)
+    fixDefName (TopName name)           = TopName (fixName name)
+
+    fixTypeName = mkName . fixTypeName' . nameBase
+    -- we coerce IPFS (and similar) to Ipfs so
+    -- camelCase namer is happy
+    -- and we don't have to rename our type or fields
+    fixTypeName' "IPFSAdd"       = "IpfsAdd"
+    fixTypeName' "IPFSPinChange" = "IpfsPinChange"
+    fixTypeName' "IPFSPin"       = "IpfsPin"
+    fixTypeName' "URLVersion"    = "UrlVersion"
+    fixTypeName' x               = x
+
+    fixName = mkName . fixName' . nameBase
+    fixName' "type" = "type_"
+    fixName' n      = n
diff --git a/src/Blockfrost/Util/Pagination.hs b/src/Blockfrost/Util/Pagination.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Util/Pagination.hs
@@ -0,0 +1,54 @@
+-- | Pagination utilities
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.Util.Pagination
+  ( Paged (..)
+  , Pagination
+  , PaginationExpanded
+  , page
+  , paged
+  , nextPage
+  , maxPageSize
+  ) where
+
+import Data.Default.Class
+import Servant.API (QueryParam, (:>))
+
+-- | Pagination parameters
+data Paged = Paged
+  { countPerPage :: Int -- ^ Count of results per page
+  , pageNumber   :: Int -- ^ Page number
+  } deriving (Eq, Ord, Show)
+
+-- | Maximum number of items per page
+maxPageSize :: Int
+maxPageSize = 100
+
+instance Default Paged where
+  def = Paged maxPageSize 1
+
+-- | Default `Paged` at specific page number
+page :: Int -> Paged
+page n | n >= 1 = def { pageNumber = n }
+page _ = error "Page number not in range [1..]"
+
+-- | Construct `Paged` from page size and page number.
+--
+-- Throws error on invalid values.
+paged :: Int -> Int -> Paged
+paged size _ | size > maxPageSize = error "Page size exceeds 100"
+paged _ n | n < 1 = error "Page number not in range [1..]"
+paged size n = Paged size n
+
+-- | Increment page number
+nextPage :: Paged -> Paged
+nextPage p = p { pageNumber = 1 + pageNumber p }
+
+-- | Adds Pagination to an API
+data Pagination
+
+type PaginationExpanded subApi =
+     QueryParam "count" Int
+  :> QueryParam "page" Int
+  :> subApi
diff --git a/src/Blockfrost/Util/Sorting.hs b/src/Blockfrost/Util/Sorting.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Util/Sorting.hs
@@ -0,0 +1,45 @@
+-- | Sorting utilities
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.Util.Sorting
+  ( SortOrder (..)
+  , Sorting
+  , SortingExpanded
+  , asc
+  , desc
+  ) where
+
+import Data.Default.Class
+import Servant.API (FromHttpApiData (..), QueryParam, ToHttpApiData (..), (:>))
+
+data SortOrder = Ascending | Descending
+  deriving (Eq, Show, Ord)
+
+-- | @Ascending@ shortcut
+asc :: SortOrder
+asc = Ascending
+
+-- | @Descending@ shortcut
+desc :: SortOrder
+desc = Descending
+
+instance Default SortOrder where
+  def = Ascending
+
+instance ToHttpApiData SortOrder where
+  toUrlPiece Ascending  = "asc"
+  toUrlPiece Descending = "desc"
+
+instance FromHttpApiData SortOrder where
+  parseUrlPiece "asc"  = Right Ascending
+  parseUrlPiece "desc" = Right Descending
+  parseUrlPiece z      = Left ("unknown SortOrder: " <> z)
+
+-- | Adds sorting to an API
+data Sorting
+
+-- | Adds sorting to an API
+type SortingExpanded a =
+  QueryParam "order" SortOrder
+  :> a
diff --git a/src/Blockfrost/Util/Tag.hs b/src/Blockfrost/Util/Tag.hs
new file mode 100644
--- /dev/null
+++ b/src/Blockfrost/Util/Tag.hs
@@ -0,0 +1,13 @@
+-- | Tag for OpenAPI 3 docs
+
+{-# OPTIONS_HADDOCK hide #-}
+
+module Blockfrost.Util.Tag
+  ( Tag
+  ) where
+
+import GHC.TypeLits (Symbol)
+
+-- | Attaches a tag to documentation.
+-- Server / client implementations remains intact.
+data Tag (name :: Symbol)
diff --git a/test/APISpec.hs b/test/APISpec.hs
new file mode 100644
--- /dev/null
+++ b/test/APISpec.hs
@@ -0,0 +1,135 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module APISpec
+  where
+
+import Data.Aeson (decode, eitherDecode, encode)
+import Data.Text (Text)
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_sample :: Spec
+spec_sample = do
+  it "parses server time sample" $ do
+    decode serverTimeSample
+    `shouldBe`
+    Just (ServerTime 1603400958.947)
+
+  it "parses block sample" $ do
+    eitherDecode blockSample
+    `shouldBe`
+    Right blockExpected
+
+  it "parses metrics sample" $ do
+    eitherDecode metricsSample
+    `shouldBe`
+    Right metricsExpected
+
+  it "parses metrics endpoints sample" $ do
+    eitherDecode metricsEndpointsSample
+    `shouldBe`
+    Right metricsEndpointsExpected
+
+serverTimeSample = [r| { "server_time": 1603400958947 } |]
+
+blockSample = [r|
+{
+    "time": 1641338934,
+    "height": 15243593,
+    "hash": "4ea1ba291e8eef538635a53e59fddba7810d1679631cc3aed7c8e6c4091a516a",
+    "slot": 412162133,
+    "epoch": 425,
+    "epoch_slot": 12,
+    "slot_leader": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2qnikdy",
+    "size": 3,
+    "tx_count": 1,
+    "output": "128314491794",
+    "fees": "592661",
+    "block_vrf": "vrf_vk1wf2k6lhujezqcfe00l6zetxpnmh9n6mwhpmhm0dvfh3fxgmdnrfqkms8ty",
+    "previous_block": "43ebccb3ac72c7cebd0d9b755a4b08412c9f5dcb81b8a0ad1e3c197d29d47b05",
+    "next_block": "8367f026cf4b03e116ff8ee5daf149b55ba5a6ec6dec04803b8dc317721d15fa",
+    "confirmations": 4698
+}
+|]
+
+blockExpected = Block
+  { _blockTime = 1641338934
+  , _blockHeight = pure 15243593
+  , _blockHash = "4ea1ba291e8eef538635a53e59fddba7810d1679631cc3aed7c8e6c4091a516a"
+  , _blockSlot = pure 412162133
+  , _blockEpoch = pure 425
+  , _blockEpochSlot = pure 12
+  , _blockSlotLeader = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2qnikdy"
+  , _blockSize = 3
+  , _blockTxCount = 1
+  , _blockOutput = pure $ 128314491794
+  , _blockFees = pure $ 592661
+  , _blockBlockVrf = pure "vrf_vk1wf2k6lhujezqcfe00l6zetxpnmh9n6mwhpmhm0dvfh3fxgmdnrfqkms8ty"
+  , _blockPreviousBlock = pure "43ebccb3ac72c7cebd0d9b755a4b08412c9f5dcb81b8a0ad1e3c197d29d47b05"
+  , _blockNextBlock = pure "8367f026cf4b03e116ff8ee5daf149b55ba5a6ec6dec04803b8dc317721d15fa"
+  , _blockConfirmations = 4698
+  }
+
+metricsSample = [r|
+[
+  {
+    "time": 1612543884,
+    "calls": 42
+  },
+  {
+    "time": 1614523884,
+    "calls": 6942
+  }
+]
+|]
+
+metricsExpected =
+  [ Metric 1612543884 42
+  , Metric 1614523884 6942
+  ]
+
+metricsEndpointsSample = [r|
+[
+  {
+    "time": 1612543814,
+    "calls": 182,
+    "endpoint": "block"
+  },
+  {
+    "time": 1612543814,
+    "calls": 42,
+    "endpoint": "epoch"
+  },
+  {
+    "time": 1612543812,
+    "calls": 775,
+    "endpoint": "block"
+  },
+  {
+    "time": 1612523884,
+    "calls": 4,
+    "endpoint": "epoch"
+  },
+  {
+    "time": 1612553884,
+    "calls": 89794,
+    "endpoint": "block"
+  }
+]
+|]
+
+metricsEndpointsExpected :: [(Text, Metric)]
+metricsEndpointsExpected =
+  [ ("block", Metric 1612543814 182)
+  , ("epoch", Metric 1612543814 42)
+  , ("block", Metric 1612543812 775)
+  , ("epoch", Metric 1612523884 4)
+  , ("block", Metric 1612553884 89794)
+  ]
diff --git a/test/AmountSpec.hs b/test/AmountSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/AmountSpec.hs
@@ -0,0 +1,76 @@
+-- | Amount (de)serialization
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module AmountSpec
+  where
+
+import Blockfrost.Types.Shared.Amount
+import Data.Aeson (decode, encode)
+import Data.Default
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+spec_sample :: Spec
+spec_sample = do
+  it "my samples decodes to expected value" $ do
+    decode mySamples `shouldBe` Just mySamplesExpected
+  it "doc samples decodes to expected value" $ do
+    decode docSamples `shouldBe` Just docSamplesExpected
+
+mySamples = [r|
+[
+    { "unit":"lovelace"
+    , "quantity":"42000000"
+    }
+
+  , { "unit":"b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+    , "quantity":"12"
+    }
+
+  , { "unit":"6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad"
+    , "quantity":"18605647"
+    }
+]
+|]
+
+mySamplesExpected =
+  [ AdaAmount 42000000
+  , AssetAmount
+      $ Money.toSomeDiscrete
+        (12 :: Money.Discrete'
+                  "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+                  '(1,1))
+  , AssetAmount
+      $ Money.toSomeDiscrete
+        (18605647 :: Money.Discrete'
+                        "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad"
+                        '(1,1))
+  ]
+
+docSamples = [r|
+[
+    {
+        "unit": "lovelace",
+        "quantity": "42000000"
+    },
+    {
+        "unit": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e",
+        "quantity": "12"
+    }
+]
+|]
+
+docSamplesExpected =
+  [ AdaAmount 42000000
+  , AssetAmount
+      $ Money.toSomeDiscrete
+        (12 :: Money.Discrete'
+                  "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+                  '(1,1))
+  ]
diff --git a/test/Cardano/Accounts.hs b/test/Cardano/Accounts.hs
new file mode 100644
--- /dev/null
+++ b/test/Cardano/Accounts.hs
@@ -0,0 +1,266 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cardano.Accounts
+  where
+
+import Data.Aeson (decode, eitherDecode, encode)
+import Data.Text (Text)
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_sample :: Spec
+spec_sample = do
+  it "parses account sample" $ do
+    eitherDecode accountSample
+    `shouldBe`
+    Right accountExpected
+
+  it "parses account rewards sample" $ do
+    eitherDecode accountRewardsSample
+    `shouldBe`
+    Right accountRewardsExpected
+
+  it "parses account history sample" $ do
+    eitherDecode accountHistorySample
+    `shouldBe`
+    Right accountHistoryExpected
+
+  it "parses account delegation history sample" $ do
+    eitherDecode accountDelegationHistorySample
+    `shouldBe`
+    Right accountDelegationHistoryExpected
+
+  it "parses account withdrawal history sample" $ do
+    eitherDecode accountWithdrawalsSample
+    `shouldBe`
+    Right accountWithdrawalsExpected
+
+  it "parses account mirs sample" $ do
+    eitherDecode accountMirsSample
+    `shouldBe`
+    Right accountMirsExpected
+
+accountSample = [r|
+{
+    "stake_address": "stake1ux3g2c9dx2nhhehyrezyxpkstartcqmu9hk63qgfkccw5rqttygt7",
+    "active": true,
+    "active_epoch": 412,
+    "controlled_amount": "619154618165",
+    "rewards_sum": "319154618165",
+    "withdrawals_sum": "12125369253",
+    "reserves_sum": "319154618165",
+    "treasury_sum": "12000000",
+    "withdrawable_amount": "319154618165",
+    "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+}
+|]
+
+accountExpected = AccountInfo
+    { _accountInfoStakeAddress = "stake1ux3g2c9dx2nhhehyrezyxpkstartcqmu9hk63qgfkccw5rqttygt7"
+    , _accountInfoActive = True
+    , _accountInfoActiveEpoch = 412
+    , _accountInfoControlledAmount = 619154618165
+    , _accountInfoRewardsSum = 319154618165
+    , _accountInfoWithdrawalsSum = 12125369253
+    , _accountInfoReservesSum = 319154618165
+    , _accountInfoTreasurySum = 12000000
+    , _accountInfoWithdrawableAmount = 319154618165
+    , _accountInfoPoolId = pure "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    }
+
+accountRewardsSample = [r|
+[
+    {
+        "epoch": 215,
+        "amount": "12695385",
+        "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    },
+    {
+        "epoch": 216,
+        "amount": "12695385",
+        "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    },
+    {
+        "epoch": 216,
+        "amount": "3586329",
+        "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    },
+    {
+        "epoch": 217,
+        "amount": "0",
+        "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    },
+    {
+        "epoch": 218,
+        "amount": "1395265",
+        "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    }
+]
+|]
+
+accountRewardsExpected =
+  [ AccountReward
+      { _accountRewardEpoch = 215
+      , _accountRewardAmount = 12695385
+      , _accountRewardPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      }
+  , AccountReward
+      { _accountRewardEpoch = 216
+      , _accountRewardAmount = 12695385
+      , _accountRewardPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      }
+  , AccountReward
+      { _accountRewardEpoch = 216
+      , _accountRewardAmount = 3586329
+      , _accountRewardPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      }
+  , AccountReward
+      { _accountRewardEpoch = 217
+      , _accountRewardAmount = 0
+      , _accountRewardPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      }
+  , AccountReward
+      { _accountRewardEpoch = 218
+      , _accountRewardAmount = 1395265
+      , _accountRewardPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      }
+  ]
+
+accountHistorySample = [r|
+[
+    {
+        "active_epoch": 210,
+        "amount": "12695385",
+        "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    },
+    {
+        "active_epoch": 211,
+        "amount": "22695385",
+        "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    }
+]
+|]
+
+accountHistoryExpected =
+  [ AccountHistory
+      { _accountHistoryActiveEpoch = 210
+      , _accountHistoryAmount = 12695385
+      , _accountHistoryPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      }
+  , AccountHistory
+      { _accountHistoryActiveEpoch = 211
+      , _accountHistoryAmount = 22695385
+      , _accountHistoryPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      }
+  ]
+
+accountDelegationHistorySample = [r|
+[
+    {
+        "active_epoch": 210,
+        "tx_hash": "2dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531",
+        "amount": "12695385",
+        "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    },
+    {
+        "active_epoch": 242,
+        "tx_hash": "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dde628516157f0",
+        "amount": "12691385",
+        "pool_id": "pool1kchver88u3kygsak8wgll7htr8uxn5v35lfrsyy842nkscrzyvj"
+    }
+]
+|]
+
+accountDelegationHistoryExpected =
+  [ AccountDelegation
+      { _accountDelegationActiveEpoch = 210
+      , _accountDelegationTxHash = "2dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531"
+      , _accountDelegationAmount = 12695385
+      , _accountDelegationPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+      }
+  , AccountDelegation
+      { _accountDelegationActiveEpoch = 242
+      , _accountDelegationTxHash = "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dde628516157f0"
+      , _accountDelegationAmount = 12691385
+      , _accountDelegationPoolId = "pool1kchver88u3kygsak8wgll7htr8uxn5v35lfrsyy842nkscrzyvj"
+      }
+  ]
+
+accountRegistrationSample = [r|
+[
+    {
+        "tx_hash": "2dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531",
+        "action": "registered"
+    },
+    {
+        "tx_hash": "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dde628516157f0",
+        "action": "deregistered"
+    }
+]
+|]
+
+accountRegistrationExpected =
+  [ AccountRegistration
+      { _accountRegistrationAction = Registered
+      , _accountRegistrationTxHash = "2dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531"
+      }
+  , AccountRegistration
+      { _accountRegistrationAction = Deregistered
+      , _accountRegistrationTxHash = "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dde628516157f0"
+      }
+  ]
+
+accountWithdrawalsSample = [r|
+[
+    {
+        "tx_hash": "48a9625c841eea0dd2bb6cf551eabe6523b7290c9ce34be74eedef2dd8f7ecc5",
+        "amount": "454541212442"
+    },
+    {
+        "tx_hash": "4230b0cbccf6f449f0847d8ad1d634a7a49df60d8c142bb8cc2dbc8ca03d9e34",
+        "amount": "97846969"
+    }
+]
+|]
+
+accountWithdrawalsExpected =
+  [ AccountWithdrawal
+      { _accountWithdrawalAmount = 454541212442
+      , _accountWithdrawalTxHash = "48a9625c841eea0dd2bb6cf551eabe6523b7290c9ce34be74eedef2dd8f7ecc5"
+      }
+  , AccountWithdrawal
+      { _accountWithdrawalAmount = 97846969
+      , _accountWithdrawalTxHash = "4230b0cbccf6f449f0847d8ad1d634a7a49df60d8c142bb8cc2dbc8ca03d9e34"
+      }
+  ]
+
+accountMirsSample = [r|
+[
+    {
+        "tx_hash": "69705bba1d687a816ff5a04ec0c358a1f1ef075ab7f9c6cc2763e792581cec6d",
+        "amount": "2193707473"
+    },
+    {
+        "tx_hash": "baaa77b63d4d7d2bb3ab02c9b85978c2092c336dede7f59e31ad65452d510c13",
+        "amount": "14520198574"
+    }
+]
+|]
+
+accountMirsExpected =
+  [ AccountMir
+      { _accountMirAmount = 2193707473
+      , _accountMirTxHash = "69705bba1d687a816ff5a04ec0c358a1f1ef075ab7f9c6cc2763e792581cec6d"
+      }
+  , AccountMir
+      { _accountMirAmount = 14520198574
+      , _accountMirTxHash = "baaa77b63d4d7d2bb3ab02c9b85978c2092c336dede7f59e31ad65452d510c13"
+      }
+  ]
diff --git a/test/Cardano/Addresses.hs b/test/Cardano/Addresses.hs
new file mode 100644
--- /dev/null
+++ b/test/Cardano/Addresses.hs
@@ -0,0 +1,231 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cardano.Addresses
+  where
+
+import Data.Aeson (decode, eitherDecode, encode, object, (.=))
+import Data.Text (Text)
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_sample :: Spec
+spec_sample = do
+  it "parses address info sample" $ do
+    eitherDecode addressInfoSample
+    `shouldBe`
+    Right addressInfoExpected
+
+  it "parses address info sample" $ do
+    eitherDecode addressInfoSample
+    `shouldBe`
+    Right addressInfoExpected
+
+  it "parses address details sample" $ do
+    eitherDecode addressDetailsSample
+    `shouldBe`
+    Right addressDetailsExpected
+
+  it "parses address utxos sample" $ do
+    eitherDecode addressUTXOsSample
+    `shouldBe`
+    Right addressUTXOsExpected
+
+  it "parses address transactions sample" $ do
+    eitherDecode addressTransactionsSample
+    `shouldBe`
+    Right addressTransactionsExpected
+
+addressInfoSample = [r|
+{
+  "address": "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz",
+  "amount": [
+    {
+      "unit": "lovelace",
+      "quantity": "42000000"
+    },
+    {
+      "unit": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e",
+      "quantity": "12"
+    }
+  ],
+  "stake_address": "stake1ux3g2c9dx2nhhehyrezyxpkstartcqmu9hk63qgfkccw5rqttygt7",
+  "type": "shelley"
+}
+|]
+
+addressInfoExpected =
+  AddressInfo
+    { _addressInfoAddress = "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz"
+    , _addressInfoAmount =
+      [ AdaAmount 42000000
+      , AssetAmount
+          $ Money.mkSomeDiscrete
+              "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+              unitScale
+              12
+      ]
+    , _addressInfoStakeAddress = pure "stake1ux3g2c9dx2nhhehyrezyxpkstartcqmu9hk63qgfkccw5rqttygt7"
+    , _addressInfoType = Shelley
+    }
+
+addressDetailsSample = [r|
+{
+  "address": "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz",
+  "received_sum": [
+    {
+      "unit": "lovelace",
+      "quantity": "42000000"
+    },
+    {
+      "unit": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e",
+      "quantity": "12"
+    }
+  ],
+  "sent_sum": [
+    {
+      "unit": "lovelace",
+      "quantity": "42000000"
+    },
+    {
+      "unit": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e",
+      "quantity": "12"
+    }
+  ],
+  "tx_count": 12
+}
+|]
+
+addressDetailsExpected =
+    AddressDetails
+    { _addressDetailsAddress = "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz"
+    , _addressDetailsReceivedSum = amounts
+    , _addressDetailsSentSum = amounts
+    , _addressDetailsTxCount = 12
+    }
+    where amounts =
+            [ AdaAmount 42000000
+            , AssetAmount
+                $ Money.mkSomeDiscrete
+                        "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+                        unitScale
+                        12
+            ]
+
+
+addressUTXOsSample = [r|
+[
+    {
+        "tx_hash": "39a7a284c2a0948189dc45dec670211cd4d72f7b66c5726c08d9b3df11e44d58",
+        "output_index": 0,
+        "amount":
+        [
+            {
+                "unit": "lovelace",
+                "quantity": "42000000"
+            }
+        ],
+        "block": "7eb8e27d18686c7db9a18f8bbcfe34e3fed6e047afaa2d969904d15e934847e6"
+    },
+    {
+        "tx_hash": "4c4e67bafa15e742c13c592b65c8f74c769cd7d9af04c848099672d1ba391b49",
+        "output_index": 0,
+        "amount":
+        [
+            {
+                "unit": "lovelace",
+                "quantity": "729235000"
+            }
+        ],
+        "block": "953f1b80eb7c11a7ffcd67cbd4fde66e824a451aca5a4065725e5174b81685b7"
+    },
+    {
+        "tx_hash": "768c63e27a1c816a83dc7b07e78af673b2400de8849ea7e7b734ae1333d100d2",
+        "output_index": 1,
+        "amount":
+        [
+            {
+                "unit": "lovelace",
+                "quantity": "42000000"
+            },
+            {
+                "unit": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e",
+                "quantity": "12"
+            }
+        ],
+        "block": "5c571f83fe6c784d3fbc223792627ccf0eea96773100f9aedecf8b1eda4544d7"
+    }
+]
+|]
+
+addressUTXOsExpected =
+  [ AddressUTXO
+    { _addressUTXOTxHash = "39a7a284c2a0948189dc45dec670211cd4d72f7b66c5726c08d9b3df11e44d58"
+    , _addressUTXOOutputIndex = 0
+    , _addressUTXOAmount = [ AdaAmount 42000000 ]
+    , _addressUTXOBlock = "7eb8e27d18686c7db9a18f8bbcfe34e3fed6e047afaa2d969904d15e934847e6"
+    }
+  , AddressUTXO
+    { _addressUTXOTxHash = "4c4e67bafa15e742c13c592b65c8f74c769cd7d9af04c848099672d1ba391b49"
+    , _addressUTXOOutputIndex = 0
+    , _addressUTXOAmount = [ AdaAmount 729235000 ]
+    , _addressUTXOBlock = "953f1b80eb7c11a7ffcd67cbd4fde66e824a451aca5a4065725e5174b81685b7"
+    }
+  , AddressUTXO
+    { _addressUTXOTxHash = "768c63e27a1c816a83dc7b07e78af673b2400de8849ea7e7b734ae1333d100d2"
+    , _addressUTXOOutputIndex = 1
+    , _addressUTXOAmount =
+        [ AdaAmount 42000000
+        , AssetAmount
+            $ Money.mkSomeDiscrete
+                "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+                 unitScale
+                 12
+        ]
+    , _addressUTXOBlock = "5c571f83fe6c784d3fbc223792627ccf0eea96773100f9aedecf8b1eda4544d7"
+    }
+  ]
+
+addressTransactionsSample = [r|
+[
+    {
+        "tx_hash": "8788591983aa73981fc92d6cddbbe643959f5a784e84b8bee0db15823f575a5b",
+        "tx_index": 6,
+        "block_height": 69
+    },
+    {
+        "tx_hash": "52e748c4dec58b687b90b0b40d383b9fe1f24c1a833b7395cdf07dd67859f46f",
+        "tx_index": 9,
+        "block_height": 4547
+    },
+    {
+        "tx_hash": "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b",
+        "tx_index": 0,
+        "block_height": 564654
+    }
+]
+|]
+
+addressTransactionsExpected =
+  [ AddressTransaction
+    { _addressTransactionTxHash = "8788591983aa73981fc92d6cddbbe643959f5a784e84b8bee0db15823f575a5b"
+    , _addressTransactionTxIndex = 6
+    , _addressTransactionBlockHeight = 69
+    }
+  , AddressTransaction
+    { _addressTransactionTxHash = "52e748c4dec58b687b90b0b40d383b9fe1f24c1a833b7395cdf07dd67859f46f"
+    , _addressTransactionTxIndex = 9
+    , _addressTransactionBlockHeight = 4547
+    }
+  , AddressTransaction
+    { _addressTransactionTxHash = "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b"
+    , _addressTransactionTxIndex = 0
+    , _addressTransactionBlockHeight = 564654
+    }
+  ]
diff --git a/test/Cardano/Assets.hs b/test/Cardano/Assets.hs
new file mode 100644
--- /dev/null
+++ b/test/Cardano/Assets.hs
@@ -0,0 +1,233 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cardano.Assets
+  where
+
+import Data.Aeson (decode, eitherDecode, encode)
+import Data.Text (Text)
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_assets :: Spec
+spec_assets = do
+  it "parses assets sample" $ do
+    eitherDecode assetInfoSample
+    `shouldBe`
+    Right assetInfoExpected
+
+  it "parses assets details sample" $ do
+    eitherDecode assetDetailsSample
+    `shouldBe`
+    Right assetDetailsExpected
+
+  it "parses assets history sample" $ do
+    eitherDecode assetHistorySample
+    `shouldBe`
+    Right assetHistoryExpected
+
+  it "parses assets transaction sample" $ do
+    eitherDecode assetTransactionsSample
+      `shouldBe`
+      Right assetTransactionsExpected
+
+  it "parses assets adresses sample" $ do
+    eitherDecode assetAddressesSample
+    `shouldBe`
+    Right assetAddressesExpected
+
+assetInfoSample = [r|
+[
+  {
+    "asset": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e",
+    "quantity": "1"
+  },
+  {
+    "asset": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e75d",
+    "quantity": "100000"
+  },
+  {
+    "asset": "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad",
+    "quantity": "18605647"
+  }
+]
+|]
+
+assetInfoExpected =
+  [ AssetInfo
+      { _assetInfoAsset = "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+      , _assetInfoQuantity = 1
+      }
+  , AssetInfo
+      { _assetInfoAsset = "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e75d"
+      , _assetInfoQuantity = 100000
+      }
+  , AssetInfo
+      { _assetInfoAsset = "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad"
+      , _assetInfoQuantity = 18605647
+      }
+  ]
+
+
+assetDetailsSample = [r|
+{
+  "asset": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e",
+  "policy_id": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7",
+  "asset_name": "6e7574636f696e",
+  "fingerprint": "asset1pkpwyknlvul7az0xx8czhl60pyel45rpje4z8w",
+  "quantity": "12000",
+  "initial_mint_tx_hash": "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad",
+  "mint_or_burn_count": 1,
+  "onchain_metadata": {
+    "name": "My NFT token",
+    "image": "ipfs://ipfs/QmfKyJ4tuvHowwKQCbCHj4L5T3fSj8cjs7Aau8V7BWv226"
+  },
+  "metadata": {
+    "name": "nutcoin",
+    "description": "The Nut Coin",
+    "ticker": "nutc",
+    "url": "https://www.stakenuts.com/",
+    "logo": "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",
+    "decimals": 6
+  }
+}
+|]
+
+assetDetailsExpected =
+  AssetDetails
+    { _assetDetailsAsset = "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+    , _assetDetailsPolicyId = "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7"
+    , _assetDetailsAssetName = pure "6e7574636f696e"
+    , _assetDetailsFingerprint = "asset1pkpwyknlvul7az0xx8czhl60pyel45rpje4z8w"
+    , _assetDetailsQuantity = 12000
+    , _assetDetailsInitialMintTxHash = "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad"
+    , _assetDetailsMintOrBurnCount = 1
+    , _assetDetailsOnchainMetadata = pure $
+        AssetOnChainMetadata
+          { _assetOnChainMetadataName = "My NFT token"
+          , _assetOnChainMetadataImage = "ipfs://ipfs/QmfKyJ4tuvHowwKQCbCHj4L5T3fSj8cjs7Aau8V7BWv226"
+          }
+    , _assetDetailsMetadata = pure $
+        AssetMetadata
+          { _assetMetadataName = "nutcoin"
+          , _assetMetadataDescription = "The Nut Coin"
+          , _assetMetadataTicker = pure "nutc"
+          , _assetMetadataUrl = pure "https://www.stakenuts.com/"
+          , _assetMetadataLogo =  pure "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"
+          , _assetMetadataDecimals = pure 6
+          }
+    }
+
+assetHistorySample = [r|
+[
+  {
+    "tx_hash": "2dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531",
+    "amount": "10",
+    "action": "minted"
+  },
+  {
+    "tx_hash": "9c190bc1ac88b2ab0c05a82d7de8b71b67a9316377e865748a89d4426c0d3005",
+    "amount": "5",
+    "action": "burned"
+  },
+  {
+    "tx_hash": "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dde628516157f0",
+    "amount": "5",
+    "action": "burned"
+  }
+]
+|]
+
+assetHistoryExpected =
+  [ AssetHistory
+      { _assetHistoryTxHash = "2dd15e0ef6e6a17841cb9541c27724072ce4d4b79b91e58432fbaa32d9572531"
+      , _assetHistoryAmount = 10
+      , _assetHistoryAction = Minted
+      }
+  , AssetHistory
+      { _assetHistoryTxHash = "9c190bc1ac88b2ab0c05a82d7de8b71b67a9316377e865748a89d4426c0d3005"
+      , _assetHistoryAmount = 5
+      , _assetHistoryAction = Burned
+      }
+  , AssetHistory
+      { _assetHistoryTxHash = "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dde628516157f0"
+      , _assetHistoryAmount = 5
+      , _assetHistoryAction = Burned
+      }
+  ]
+
+assetTransactionsSample = [r|
+[
+  {
+    "tx_hash": "8788591983aa73981fc92d6cddbbe643959f5a784e84b8bee0db15823f575a5b",
+    "tx_index": 6,
+    "block_height": 69
+  },
+  {
+    "tx_hash": "52e748c4dec58b687b90b0b40d383b9fe1f24c1a833b7395cdf07dd67859f46f",
+    "tx_index": 9,
+    "block_height": 4547
+  },
+  {
+    "tx_hash": "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b",
+    "tx_index": 0,
+    "block_height": 564654
+  }
+]
+|]
+
+assetTransactionsExpected =
+  [ AssetTransaction
+      { _assetTransactionTxHash = "8788591983aa73981fc92d6cddbbe643959f5a784e84b8bee0db15823f575a5b"
+      , _assetTransactionTxIndex = 6
+      , _assetTransactionBlockHeight = 69
+      }
+  , AssetTransaction
+      { _assetTransactionTxHash = "52e748c4dec58b687b90b0b40d383b9fe1f24c1a833b7395cdf07dd67859f46f"
+      , _assetTransactionTxIndex = 9
+      , _assetTransactionBlockHeight = 4547
+      }
+   , AssetTransaction
+      { _assetTransactionTxHash = "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b"
+      , _assetTransactionTxIndex = 0
+      , _assetTransactionBlockHeight = 564654
+      }
+  ]
+
+assetAddressesSample = [r|
+[
+  {
+    "address": "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz",
+    "quantity": "1"
+  },
+  {
+    "address": "addr1qyhr4exrgavdcn3qhfcc9f939fzsch2re5ry9cwvcdyh4x4re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qdpvhza",
+    "quantity": "100000"
+  },
+  {
+    "address": "addr1q8zup8m9ue3p98kxlxl9q8rnyan8hw3ul282tsl9s326dfj088lvedv4zckcj24arcpasr0gua4c5gq4zw2rpcpjk2lq8cmd9l",
+    "quantity": "18605647"
+  }
+]
+|]
+
+assetAddressesExpected =
+  [ AssetAddress
+      { _assetAddressAddress = "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz"
+      , _assetAddressQuantity = 1
+      }
+  , AssetAddress
+      { _assetAddressAddress = "addr1qyhr4exrgavdcn3qhfcc9f939fzsch2re5ry9cwvcdyh4x4re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qdpvhza"
+      , _assetAddressQuantity = 100000
+      }
+   , AssetAddress
+      { _assetAddressAddress = "addr1q8zup8m9ue3p98kxlxl9q8rnyan8hw3ul282tsl9s326dfj088lvedv4zckcj24arcpasr0gua4c5gq4zw2rpcpjk2lq8cmd9l"
+      , _assetAddressQuantity = 18605647
+      }
+  ]
diff --git a/test/Cardano/Epochs.hs b/test/Cardano/Epochs.hs
new file mode 100644
--- /dev/null
+++ b/test/Cardano/Epochs.hs
@@ -0,0 +1,146 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cardano.Epochs
+  where
+
+import Data.Aeson (decode, eitherDecode, encode)
+import Data.Text (Text)
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_epochs :: Spec
+spec_epochs = do
+  it "parses epoch info sample" $ do
+    eitherDecode epochInfoSample
+    `shouldBe`
+    Right epochInfoExpected
+
+  it "parses protocol params sample" $ do
+    eitherDecode protocolParamsSample
+    `shouldBe`
+    Right protocolParamsExpected
+
+  it "parses stake distribution sample" $ do
+    eitherDecode stakeDistributionSample
+    `shouldBe`
+    Right stakeDistributionExpected
+
+  it "parses pool stake distribution sample" $ do
+    eitherDecode poolStakeDistributionSample
+    `shouldBe`
+    Right poolStakeDistributionExpected
+
+epochInfoSample = [r|
+{
+    "epoch": 225,
+    "start_time": 1603403091,
+    "end_time": 1603835086,
+    "first_block_time": 1603403092,
+    "last_block_time": 1603835084,
+    "block_count": 21298,
+    "tx_count": 17856,
+    "output": "7849943934049314",
+    "fees": "4203312194",
+    "active_stake": "784953934049314"
+}
+|]
+
+epochInfoExpected =
+  EpochInfo
+    { _epochInfoEpoch = 225
+    , _epochInfoStartTime = 1603403091
+    , _epochInfoEndTime = 1603835086
+    , _epochInfoFirstBlockTime = 1603403092
+    , _epochInfoLastBlockTime = 1603835084
+    , _epochInfoBlockCount = 21298
+    , _epochInfoTxCount = 17856
+    , _epochInfoOutput = 7849943934049314
+    , _epochInfoFees = 4203312194
+    , _epochInfoActiveStake = pure 784953934049314
+    }
+
+protocolParamsSample = [r|
+{
+    "epoch": 225,
+    "min_fee_a": 44,
+    "min_fee_b": 155381,
+    "max_block_size": 65536,
+    "max_tx_size": 16384,
+    "max_block_header_size": 1100,
+    "key_deposit": "2000000",
+    "pool_deposit": "500000000",
+    "e_max": 18,
+    "n_opt": 150,
+    "a0": 0.3,
+    "rho": 0.003,
+    "tau": 0.2,
+    "decentralisation_param": 0.5,
+    "extra_entropy": null,
+    "protocol_major_ver": 2,
+    "protocol_minor_ver": 0,
+    "min_utxo": "1000000",
+    "min_pool_cost": "340000000",
+    "nonce": "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81"
+}
+|]
+
+protocolParamsExpected =
+  ProtocolParams
+    { _protocolParamsEpoch = 225
+    , _protocolParamsMinFeeA = 44
+    , _protocolParamsMinFeeB = 155381
+    , _protocolParamsMaxBlockSize = 65536
+    , _protocolParamsMaxTxSize = 16384
+    , _protocolParamsMaxBlockHeaderSize = 1100
+    , _protocolParamsKeyDeposit = 2000000
+    , _protocolParamsPoolDeposit = 500000000
+    , _protocolParamsEMax = 18
+    , _protocolParamsNOpt = 150
+    , _protocolParamsA0 = 0.3
+    , _protocolParamsRho = 0.003
+    , _protocolParamsTau = 0.2
+    , _protocolParamsDecentralisationParam = 0.5
+--    , _protocolParamsExtraEntropy = Nothing
+    , _protocolParamsProtocolMajorVer = 2
+    , _protocolParamsProtocolMinorVer = 0
+    , _protocolParamsMinUtxo = 1000000
+    , _protocolParamsMinPoolCost = 340000000
+    , _protocolParamsNonce = "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81"
+    }
+
+stakeDistributionSample = [r|
+{
+  "stake_address": "stake1u9l5q5jwgelgagzyt6nuaasefgmn8pd25c8e9qpeprq0tdcp0e3uk",
+  "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy",
+  "amount": "4440295078"
+}
+|]
+
+stakeDistributionExpected =
+  StakeDistribution
+    { _stakeDistributionStakeAddress = "stake1u9l5q5jwgelgagzyt6nuaasefgmn8pd25c8e9qpeprq0tdcp0e3uk"
+    , _stakeDistributionPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    , _stakeDistributionAmount = 4440295078
+    }
+
+poolStakeDistributionSample = [r|
+{
+  "stake_address": "stake1u9l5q5jwgelgagzyt6nuaasefgmn8pd25c8e9qpeprq0tdcp0e3uk",
+  "amount": "4440295078"
+}
+|]
+
+poolStakeDistributionExpected =
+  PoolStakeDistribution
+    { _poolStakeDistributionStakeAddress = "stake1u9l5q5jwgelgagzyt6nuaasefgmn8pd25c8e9qpeprq0tdcp0e3uk"
+    , _poolStakeDistributionAmount = 4440295078
+    }
+
+
diff --git a/test/Cardano/Ledger.hs b/test/Cardano/Ledger.hs
new file mode 100644
--- /dev/null
+++ b/test/Cardano/Ledger.hs
@@ -0,0 +1,54 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cardano.Ledger
+  where
+
+import Data.Aeson (decode, eitherDecode, encode)
+import Data.Text (Text)
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_ledger :: Spec
+spec_ledger = do
+  it "parses ledger genesis sample" $ do
+    eitherDecode ledgerGenesisSample
+    `shouldBe`
+    Right ledgerGenesisExpected
+
+ledgerGenesisSample = [r|
+{
+    "active_slots_coefficient": 0.05,
+    "update_quorum": 5,
+    "max_lovelace_supply": "45000000000000000",
+    "network_magic": 764824073,
+    "epoch_length": 432000,
+    "system_start": 1506203091,
+    "slots_per_kes_period": 129600,
+    "slot_length": 1,
+    "max_kes_evolutions": 62,
+    "security_param": 2160
+}
+|]
+
+ledgerGenesisExpected =
+  Genesis
+    { _genesisActiveSlotsCoefficient = 0.05
+    , _genesisUpdateQuorum = 5
+    , _genesisMaxLovelaceSupply = 45_000_000_000_000_000
+    , _genesisNetworkMagic = 764824073
+    , _genesisEpochLength = 432_000
+    , _genesisSystemStart = 1506203091
+    , _genesisSlotsPerKesPeriod = 129600
+    , _genesisSlotLength = 1
+    , _genesisMaxKesEvolutions  = 62
+    , _genesisSecurityParam  = 2160
+    }
+
+
diff --git a/test/Cardano/Metadata.hs b/test/Cardano/Metadata.hs
new file mode 100644
--- /dev/null
+++ b/test/Cardano/Metadata.hs
@@ -0,0 +1,143 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cardano.Metadata
+  where
+
+import Data.Aeson (decode, eitherDecode, encode, object, (.=))
+import Data.Text (Text)
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_metadata :: Spec
+spec_metadata = do
+  it "parses tx meta sample" $ do
+    eitherDecode txMetaSample
+    `shouldBe`
+    Right txMetaExpected
+
+  it "parses tx meta JSON sample" $ do
+    eitherDecode txMetaJSONSample
+    `shouldBe`
+    Right txMetaJSONExpected
+
+  it "parses tx meta CBOR sample" $ do
+    eitherDecode txMetaCBORSample
+    `shouldBe`
+    Right txMetaCBORExpected
+
+txMetaSample = [r|
+[
+  {
+    "label": "1990",
+    "cip10": null,
+    "count": "1"
+  },
+  {
+    "label": "1967",
+    "cip10": "nut.link metadata oracles registry",
+    "count": "3"
+  },
+  {
+    "label": "1968",
+    "cip10": "nut.link metadata oracles data points",
+    "count": "16321"
+  }
+]
+|]
+
+txMetaExpected =
+  [ TxMeta "1990" Nothing 1
+  , TxMeta "1967" (Just "nut.link metadata oracles registry") 3
+  , TxMeta "1968" (Just "nut.link metadata oracles data points") 16321
+  ]
+
+txMetaJSONSample = [r|
+[
+  {
+    "tx_hash": "257d75c8ddb0434e9b63e29ebb6241add2b835a307aa33aedba2effe09ed4ec8",
+    "json_metadata": {
+      "ADAUSD": [
+        {
+          "value": "0.10409800535729975",
+          "source": "ergoOracles"
+        }
+      ]
+    }
+  },
+  {
+    "tx_hash": "e865f2cc01ca7381cf98dcdc4de07a5e8674b8ea16e6a18e3ed60c186fde2b9c",
+    "json_metadata": {
+      "ADAUSD": [
+        {
+          "value": "0.15409850555139935",
+          "source": "ergoOracles"
+        }
+      ]
+    }
+  },
+  {
+    "tx_hash": "4237501da3cfdd53ade91e8911e764bd0699d88fd43b12f44a1f459b89bc91be",
+    "json_metadata": null
+  }
+]
+|]
+
+txMetaJSONExpected =
+  let oracleMeta val =
+        object [
+          "ADAUSD" .=
+            [ object [
+                        "value" .= (val :: Text)
+                      , "source" .= ("ergoOracles" :: Text)
+                      ]
+            ]
+        ]
+  in
+  [ TxMetaJSON
+      "257d75c8ddb0434e9b63e29ebb6241add2b835a307aa33aedba2effe09ed4ec8"
+      (Just $ oracleMeta "0.10409800535729975")
+  , TxMetaJSON
+      "e865f2cc01ca7381cf98dcdc4de07a5e8674b8ea16e6a18e3ed60c186fde2b9c"
+      (Just $ oracleMeta "0.15409850555139935")
+  , TxMetaJSON
+      "4237501da3cfdd53ade91e8911e764bd0699d88fd43b12f44a1f459b89bc91be"
+      Nothing
+  ]
+
+txMetaCBORSample = [r|
+[
+  {
+    "tx_hash": "257d75c8ddb0434e9b63e29ebb6241add2b835a307aa33aedba2effe09ed4ec8",
+    "cbor_metadata": null
+  },
+  {
+    "tx_hash": "e865f2cc01ca7381cf98dcdc4de07a5e8674b8ea16e6a18e3ed60c186fde2b9c",
+    "cbor_metadata": null
+  },
+  {
+    "tx_hash": "4237501da3cfdd53ade91e8911e764bd0699d88fd43b12f44a1f459b89bc91be",
+    "cbor_metadata": "\\xa100a16b436f6d62696e6174696f6e8601010101010c"
+  }
+]
+|]
+
+txMetaCBORExpected =
+  [ TxMetaCBOR
+      "257d75c8ddb0434e9b63e29ebb6241add2b835a307aa33aedba2effe09ed4ec8"
+      Nothing
+  , TxMetaCBOR
+      "e865f2cc01ca7381cf98dcdc4de07a5e8674b8ea16e6a18e3ed60c186fde2b9c"
+      Nothing
+  , TxMetaCBOR
+      "4237501da3cfdd53ade91e8911e764bd0699d88fd43b12f44a1f459b89bc91be"
+      (Just "\\xa100a16b436f6d62696e6174696f6e8601010101010c")
+  ]
+
+
diff --git a/test/Cardano/Network.hs b/test/Cardano/Network.hs
new file mode 100644
--- /dev/null
+++ b/test/Cardano/Network.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cardano.Network
+  where
+
+import Data.Aeson (decode, eitherDecode, encode)
+import Data.Text (Text)
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_network :: Spec
+spec_network = do
+  it "parses network sample" $ do
+    eitherDecode networkSample
+    `shouldBe`
+    Right networkExpected
+
+networkSample = [r|
+{
+  "supply": {
+    "max": "45000000000000000",
+    "total": "32890715183299160",
+    "circulating": "32412601976210393"
+  },
+  "stake": {
+    "live": "23204950463991654",
+    "active": "22210233523456321"
+  }
+}
+|]
+
+networkExpected =
+  Network
+    (NetworkSupply
+      45_000_000_000_000_000
+      32_890_715_183_299_160
+      32_412_601_976_210_393)
+    (NetworkStake
+      23_204_950_463_991_654
+      22_210_233_523_456_321)
diff --git a/test/Cardano/Pools.hs b/test/Cardano/Pools.hs
new file mode 100644
--- /dev/null
+++ b/test/Cardano/Pools.hs
@@ -0,0 +1,277 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cardano.Pools
+  where
+
+import Data.Aeson (decode, eitherDecode, encode)
+import Data.Text (Text)
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_pools :: Spec
+spec_pools = do
+  it "parses pool epoch sample" $ do
+    eitherDecode poolEpochSample
+    `shouldBe`
+    Right poolEpochExpected
+
+  it "parses pool info sample" $ do
+    eitherDecode poolInfoSample
+    `shouldBe`
+    Right poolInfoExpected
+
+  it "parses pool history sample" $ do
+    eitherDecode poolHistorySample
+    `shouldBe`
+    Right poolHistoryExpected
+
+  it "parses pool metadata sample" $ do
+    eitherDecode poolMetadataSample
+    `shouldBe`
+    Right poolMetadataExpected
+
+  it "parses empty object pool metadata sample" $ do
+    eitherDecode "{}"
+    `shouldBe`
+    Right (Nothing :: Maybe PoolMetadata)
+
+  it "parses pool relay sample" $ do
+    eitherDecode poolRelaySample
+    `shouldBe`
+    Right poolRelayExpected
+
+  it "parses pool delegators sample" $ do
+    eitherDecode poolDelegatorsSample
+    `shouldBe`
+    Right poolDelegatorsExpected
+
+  it "parses pool updates sample" $ do
+    eitherDecode poolUpdatesSample
+    `shouldBe`
+    Right poolUpdatesExpected
+
+poolEpochSample = [r|
+[
+  {
+    "pool_id": "pool19u64770wqp6s95gkajc8udheske5e6ljmpq33awxk326zjaza0q",
+    "epoch": 225
+  },
+  {
+    "pool_id": "pool1dvla4zq98hpvacv20snndupjrqhuc79zl6gjap565nku6et5zdx",
+    "epoch": 215
+  },
+  {
+    "pool_id": "pool1wvccajt4eugjtf3k0ja3exjqdj7t8egsujwhcw4tzj4rzsxzw5w",
+    "epoch": 231
+  }
+]
+|]
+
+poolEpochExpected =
+  [ PoolEpoch "pool19u64770wqp6s95gkajc8udheske5e6ljmpq33awxk326zjaza0q" 225
+  , PoolEpoch "pool1dvla4zq98hpvacv20snndupjrqhuc79zl6gjap565nku6et5zdx" 215
+  , PoolEpoch "pool1wvccajt4eugjtf3k0ja3exjqdj7t8egsujwhcw4tzj4rzsxzw5w" 231
+  ]
+
+poolInfoSample = [r|
+{
+  "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy",
+  "hex": "0f292fcaa02b8b2f9b3c8f9fd8e0bb21abedb692a6d5058df3ef2735",
+  "vrf_key": "0b5245f9934ec2151116fb8ec00f35fd00e0aa3b075c4ed12cce440f999d8233",
+  "blocks_minted": 69,
+  "live_stake": "6900000000",
+  "live_size": 0.42,
+  "live_saturation": 0.93,
+  "live_delegators": 127,
+  "active_stake": "4200000000",
+  "active_size": 0.43,
+  "declared_pledge": "5000000000",
+  "live_pledge": "5000000001",
+  "margin_cost": 0.05,
+  "fixed_cost": "340000000",
+  "reward_account": "stake1uxkptsa4lkr55jleztw43t37vgdn88l6ghclfwuxld2eykgpgvg3f",
+  "owners": [
+    "stake1u98nnlkvkk23vtvf9273uq7cph5ww6u2yq2389psuqet90sv4xv9v"
+  ],
+  "registration": [
+    "9f83e5484f543e05b52e99988272a31da373f3aab4c064c76db96643a355d9dc",
+    "7ce3b8c433bf401a190d58c8c483d8e3564dfd29ae8633c8b1b3e6c814403e95",
+    "3e6e1200ce92977c3fe5996bd4d7d7e192bcb7e231bc762f9f240c76766535b9"
+  ],
+  "retirement": [
+    "252f622976d39e646815db75a77289cf16df4ad2b287dd8e3a889ce14c13d1a8"
+  ]
+}
+|]
+
+poolInfoExpected =
+  PoolInfo
+    { _poolInfoPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    , _poolInfoHex = "0f292fcaa02b8b2f9b3c8f9fd8e0bb21abedb692a6d5058df3ef2735"
+    , _poolInfoVrfKey = "0b5245f9934ec2151116fb8ec00f35fd00e0aa3b075c4ed12cce440f999d8233"
+    , _poolInfoBlocksMinted = 69
+    , _poolInfoLiveStake = 6900000000
+    , _poolInfoLiveSize = 0.42
+    , _poolInfoLiveSaturation = 0.93
+    , _poolInfoActiveStake = 4200000000
+    , _poolInfoActiveSize = 0.43
+    , _poolInfoDeclaredPledge = 5000000000
+    , _poolInfoLivePledge = 5000000001
+    , _poolInfoMarginCost = 0.05
+    , _poolInfoFixedCost = 340000000
+    , _poolInfoRewardAccount = "stake1uxkptsa4lkr55jleztw43t37vgdn88l6ghclfwuxld2eykgpgvg3f"
+    , _poolInfoOwners = [ "stake1u98nnlkvkk23vtvf9273uq7cph5ww6u2yq2389psuqet90sv4xv9v" ]
+    , _poolInfoRegistration =
+        [ "9f83e5484f543e05b52e99988272a31da373f3aab4c064c76db96643a355d9dc"
+        , "7ce3b8c433bf401a190d58c8c483d8e3564dfd29ae8633c8b1b3e6c814403e95"
+        , "3e6e1200ce92977c3fe5996bd4d7d7e192bcb7e231bc762f9f240c76766535b9"
+        ]
+    , _poolInfoRetirement = [ "252f622976d39e646815db75a77289cf16df4ad2b287dd8e3a889ce14c13d1a8" ]
+    }
+
+
+poolHistorySample = [r|
+{
+  "epoch": 233,
+  "blocks": 22,
+  "active_stake": "20485965693569",
+  "active_size": 1.2345,
+  "delegators_count": 115,
+  "rewards": "206936253674159",
+  "fees": "1290968354"
+}
+|]
+
+poolHistoryExpected =
+  PoolHistory
+    { _poolHistoryEpoch = 233
+    , _poolHistoryBlocks = 22
+    , _poolHistoryActiveStake = 20485965693569
+    , _poolHistoryActiveSize = 1.2345
+    , _poolHistoryDelegatorsCount = 115
+    , _poolHistoryRewards = 206936253674159
+    , _poolHistoryFees = 1290968354
+    }
+
+poolMetadataSample = [r|
+{
+  "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy",
+  "hex": "0f292fcaa02b8b2f9b3c8f9fd8e0bb21abedb692a6d5058df3ef2735",
+  "url": "https://stakenuts.com/mainnet.json",
+  "hash": "47c0c68cb57f4a5b4a87bad896fc274678e7aea98e200fa14a1cb40c0cab1d8c",
+  "ticker": "NUTS",
+  "name": "Stake Nuts",
+  "description": "The best pool ever",
+  "homepage": "https://stakentus.com/"
+}
+|]
+
+poolMetadataExpected =
+  PoolMetadata
+    { _poolMetadataPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    , _poolMetadataHex = "0f292fcaa02b8b2f9b3c8f9fd8e0bb21abedb692a6d5058df3ef2735"
+    , _poolMetadataUrl = Just "https://stakenuts.com/mainnet.json"
+    , _poolMetadataHash = Just "47c0c68cb57f4a5b4a87bad896fc274678e7aea98e200fa14a1cb40c0cab1d8c"
+    , _poolMetadataTicker = Just "NUTS"
+    , _poolMetadataName = Just "Stake Nuts"
+    , _poolMetadataDescription = Just "The best pool ever"
+    , _poolMetadataHomepage = Just "https://stakentus.com/"
+    }
+
+poolRelaySample = [r|
+{
+  "ipv4": "4.4.4.4",
+  "ipv6": "https://stakenuts.com/mainnet.json",
+  "dns": "relay1.stakenuts.com",
+  "dns_srv": "_relays._tcp.relays.stakenuts.com",
+  "port": 3001
+}
+|]
+
+poolRelayExpected =
+  PoolRelay
+    { _poolRelayIpv4 = Just "4.4.4.4"
+    , _poolRelayIpv6 = Just "https://stakenuts.com/mainnet.json"
+    , _poolRelayDns = Just "relay1.stakenuts.com"
+    , _poolRelayDnsSrv = Just "_relays._tcp.relays.stakenuts.com"
+    , _poolRelayPort = 3001
+    }
+
+poolDelegatorsSample = [r|
+[
+  {
+    "address": "stake1ux4vspfvwuus9uwyp5p3f0ky7a30jq5j80jxse0fr7pa56sgn8kha",
+    "live_stake": "1137959159981411"
+  },
+  {
+    "address": "stake1uylayej7esmarzd4mk4aru37zh9yz0luj3g9fsvgpfaxulq564r5u",
+    "live_stake": "16958865648"
+  },
+  {
+    "address": "stake1u8lr2pnrgf8f7vrs9lt79hc3sxm8s2w4rwvgpncks3axx6q93d4ck",
+    "live_stake": "18605647"
+  }
+]
+|]
+
+poolDelegatorsExpected =
+  [ PoolDelegator
+      { _poolDelegatorAddress = "stake1ux4vspfvwuus9uwyp5p3f0ky7a30jq5j80jxse0fr7pa56sgn8kha"
+      , _poolDelegatorLiveStake = 1137959159981411
+      }
+  , PoolDelegator
+      { _poolDelegatorAddress = "stake1uylayej7esmarzd4mk4aru37zh9yz0luj3g9fsvgpfaxulq564r5u"
+      , _poolDelegatorLiveStake = 16958865648
+      }
+  , PoolDelegator
+      { _poolDelegatorAddress = "stake1u8lr2pnrgf8f7vrs9lt79hc3sxm8s2w4rwvgpncks3axx6q93d4ck"
+      , _poolDelegatorLiveStake = 18605647
+      }
+  ]
+
+poolUpdatesSample = [r|
+[
+  {
+    "tx_hash": "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad",
+    "cert_index": 0,
+    "action": "registered"
+  },
+  {
+    "tx_hash": "9c190bc1ac88b2ab0c05a82d7de8b71b67a9316377e865748a89d4426c0d3005",
+    "cert_index": 0,
+    "action": "deregistered"
+  },
+  {
+    "tx_hash": "e14a75b0eb2625de7055f1f580d70426311b78e0d36dd695a6bdc96c7b3d80e0",
+    "cert_index": 1,
+    "action": "registered"
+  }
+]
+|]
+
+poolUpdatesExpected =
+    [ PoolUpdate
+        { _poolUpdateTxHash = "6804edf9712d2b619edb6ac86861fe93a730693183a262b165fcc1ba1bc99cad"
+        , _poolUpdateCertIndex = 0
+        , _poolUpdateAction = PoolRegistered
+        }
+    , PoolUpdate
+        { _poolUpdateTxHash = "9c190bc1ac88b2ab0c05a82d7de8b71b67a9316377e865748a89d4426c0d3005"
+        , _poolUpdateCertIndex = 0
+        , _poolUpdateAction = PoolDeregistered
+        }
+    , PoolUpdate
+        { _poolUpdateTxHash = "e14a75b0eb2625de7055f1f580d70426311b78e0d36dd695a6bdc96c7b3d80e0"
+        , _poolUpdateCertIndex = 1
+        , _poolUpdateAction = PoolRegistered
+        }
+    ]
+
+
diff --git a/test/Cardano/Transactions.hs b/test/Cardano/Transactions.hs
new file mode 100644
--- /dev/null
+++ b/test/Cardano/Transactions.hs
@@ -0,0 +1,387 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cardano.Transactions
+  where
+
+import Data.Aeson (decode, eitherDecode, encode, object, (.=))
+import Data.Text (Text)
+import qualified Money
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_txs :: Spec
+spec_txs = do
+  it "parses transaction sample" $ do
+    eitherDecode transactionSample
+    `shouldBe`
+    Right transactionExpected
+
+  it "parses transaction utxos sample" $ do
+    eitherDecode transactionUtxosSample
+    `shouldBe`
+    Right transactionUtxosExpected
+
+  it "parses transaction stake sample" $ do
+    eitherDecode transactionStakeSample
+    `shouldBe`
+    Right transactionStakeExpected
+
+  it "parses transaction delegation sample" $ do
+    eitherDecode transactionDelegationSample
+    `shouldBe`
+    Right transactionDelegationExpected
+
+  it "parses transaction withdrawal sample" $ do
+    eitherDecode transactionWithdrawalSample
+    `shouldBe`
+    Right transactionWithdrawalExpected
+
+  it "parses transaction mir sample" $ do
+    eitherDecode transactionMirSample
+    `shouldBe`
+    Right transactionMirExpected
+
+  it "parses transaction pool update sample" $ do
+    eitherDecode transactionPoolUpdateSample
+    `shouldBe`
+    Right transactionPoolUpdateExpected
+
+  it "parses transaction pool retiring sample" $ do
+    eitherDecode transactionPoolRetiringSample
+    `shouldBe`
+    Right transactionPoolRetiringExpected
+
+  it "parses transaction meta (JSON) sample" $ do
+    eitherDecode transactionMetaJSONSample
+    `shouldBe`
+    Right transactionMetaJSONExpected
+
+  it "parses transaction meta (CBOR) sample" $ do
+    eitherDecode transactionMetaCBORSample
+    `shouldBe`
+    Right transactionMetaCBORExpected
+
+transactionSample = [r|
+{
+  "hash": "1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477",
+  "block": "356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940",
+  "block_height": 123456,
+  "slot": 42000000,
+  "index": 1,
+  "output_amount": [
+    {
+      "unit": "lovelace",
+      "quantity": "42000000"
+    },
+    {
+      "unit": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e",
+      "quantity": "12"
+    }
+  ],
+  "fees": "182485",
+  "deposit": "0",
+  "size": 433,
+  "invalid_before": null,
+  "invalid_hereafter": "13885913",
+  "utxo_count": 4,
+  "withdrawal_count": 0,
+  "mir_cert_count": 0,
+  "delegation_count": 0,
+  "stake_cert_count": 0,
+  "pool_update_count": 0,
+  "pool_retire_count": 0,
+  "asset_mint_or_burn_count": 0
+}
+|]
+
+sampleAmounts :: [Amount]
+sampleAmounts =
+  [ AdaAmount 42000000
+  , AssetAmount
+      $ Money.mkSomeDiscrete
+          "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e"
+          unitScale
+          12
+  ]
+
+transactionExpected =
+  Transaction
+    { _transactionHash = "1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477"
+    , _transactionBlock = "356b7d7dbb696ccd12775c016941057a9dc70898d87a63fc752271bb46856940"
+    , _transactionBlockHeight = 123456
+    , _transactionSlot = 42000000
+    , _transactionIndex = 1
+    , _transactionOutputAmount = sampleAmounts
+    , _transactionFees = 182485
+    , _transactionDeposit = 0
+    , _transactionSize = 433
+    , _transactionInvalidBefore = Nothing
+    , _transactionInvalidHereafter = Just "13885913"
+    , _transactionUtxoCount = 4
+    , _transactionWithdrawalCount = 0
+    , _transactionMirCertCount =  0
+    , _transactionDelegationCount = 0
+    , _transactionStakeCertCount = 0
+    , _transactionPoolUpdateCount = 0
+    , _transactionPoolRetireCount = 0
+    , _transactionAssetMintOrBurnCount = 0
+    }
+
+transactionUtxosSample = [r|
+{
+  "hash": "1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477",
+  "inputs": [
+    {
+      "address": "addr1q9ld26v2lv8wvrxxmvg90pn8n8n5k6tdst06q2s856rwmvnueldzuuqmnsye359fqrk8hwvenjnqultn7djtrlft7jnq7dy7wv",
+      "amount": [
+        {
+          "unit": "lovelace",
+          "quantity": "42000000"
+        },
+        {
+          "unit": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e",
+          "quantity": "12"
+        }
+      ],
+      "tx_hash": "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dce628516157f0",
+      "output_index": 0
+    }
+  ],
+  "outputs": [
+    {
+      "address": "addr1q9ld26v2lv8wvrxxmvg90pn8n8n5k6tdst06q2s856rwmvnueldzuuqmnsye359fqrk8hwvenjnqultn7djtrlft7jnq7dy7wv",
+      "amount": [
+        {
+          "unit": "lovelace",
+          "quantity": "42000000"
+        },
+        {
+          "unit": "b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a76e7574636f696e",
+          "quantity": "12"
+        }
+      ]
+    }
+  ]
+}
+|]
+
+utxoInSample :: UtxoInput
+utxoInSample =
+    UtxoInput
+      { _utxoInputAddress = "addr1q9ld26v2lv8wvrxxmvg90pn8n8n5k6tdst06q2s856rwmvnueldzuuqmnsye359fqrk8hwvenjnqultn7djtrlft7jnq7dy7wv"
+      , _utxoInputAmount = sampleAmounts
+      , _utxoInputTxHash = "1a0570af966fb355a7160e4f82d5a80b8681b7955f5d44bec0dce628516157f0"
+      , _utxoInputOutputIndex = 0
+      }
+
+utxoOutSample :: UtxoOutput
+utxoOutSample =
+  UtxoOutput
+    { _utxoOutputAddress = "addr1q9ld26v2lv8wvrxxmvg90pn8n8n5k6tdst06q2s856rwmvnueldzuuqmnsye359fqrk8hwvenjnqultn7djtrlft7jnq7dy7wv"
+    , _utxoOutputAmount = sampleAmounts
+    }
+
+transactionUtxosExpected =
+  TransactionUtxos
+    { _transactionUtxosHash = "1e043f100dce12d107f679685acd2fc0610e10f72a92d412794c9773d11d8477"
+    , _transactionUtxosInputs = pure utxoInSample
+    , _transactionUtxosOutputs = pure utxoOutSample
+    }
+
+transactionStakeSample = [r|
+{
+  "cert_index": 0,
+  "address": "stake1u9t3a0tcwune5xrnfjg4q7cpvjlgx9lcv0cuqf5mhfjwrvcwrulda",
+  "registration": true
+}
+|]
+
+transactionStakeExpected =
+  TransactionStake
+    { _transactionStakeCertIndex = 0
+    , _transactionStakeAddress = "stake1u9t3a0tcwune5xrnfjg4q7cpvjlgx9lcv0cuqf5mhfjwrvcwrulda"
+    , _transactionStakeRegistration = True
+    }
+
+transactionDelegationSample =  [r|
+{
+  "index": 0,
+  "cert_index": 0,
+  "address": "stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc",
+  "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy",
+  "active_epoch": 210
+}
+|]
+
+transactionDelegationExpected =
+  TransactionDelegation
+    { _transactionDelegationCertIndex = 0
+    , _transactionDelegationAddress = "stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc"
+    , _transactionDelegationPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    , _transactionDelegationActiveEpoch = 210
+    }
+
+transactionWithdrawalSample = [r|
+{
+  "address": "stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc",
+  "amount": "431833601"
+}
+|]
+
+transactionWithdrawalExpected =
+  TransactionWithdrawal
+    { _transactionWithdrawalAddress = "stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc"
+    , _transactionWithdrawalAmount = 431833601
+    }
+
+transactionMirSample = [r|
+{
+  "pot": "reserve",
+  "cert_index": 0,
+  "address": "stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc",
+  "amount": "431833601"
+}
+|]
+
+transactionMirExpected =
+  TransactionMir
+    { _transactionMirPot = Reserve
+    , _transactionMirCertIndex = 0
+    , _transactionMirAddress = "stake1u9r76ypf5fskppa0cmttas05cgcswrttn6jrq4yd7jpdnvc7gt0yc"
+    , _transactionMirAmount = 431833601
+    }
+
+transactionPoolUpdateSample = [r|
+{
+  "cert_index": 0,
+  "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy",
+  "vrf_key": "0b5245f9934ec2151116fb8ec00f35fd00e0aa3b075c4ed12cce440f999d8233",
+  "pledge": "5000000000",
+  "margin_cost": 0.05,
+  "fixed_cost": "340000000",
+  "reward_account": "stake1uxkptsa4lkr55jleztw43t37vgdn88l6ghclfwuxld2eykgpgvg3f",
+  "owners": [
+    "stake1u98nnlkvkk23vtvf9273uq7cph5ww6u2yq2389psuqet90sv4xv9v"
+  ],
+  "metadata": {
+    "url": "https://stakenuts.com/mainnet.json",
+    "hash": "47c0c68cb57f4a5b4a87bad896fc274678e7aea98e200fa14a1cb40c0cab1d8c",
+    "ticker": "NUTS",
+    "name": "Stake Nuts",
+    "description": "The best pool ever",
+    "homepage": "https://stakentus.com/"
+  },
+  "relays": [
+    {
+      "ipv4": "4.4.4.4",
+      "ipv6": "https://stakenuts.com/mainnet.json",
+      "dns": "relay1.stakenuts.com",
+      "dns_srv": "_relays._tcp.relays.stakenuts.com",
+      "port": 3001
+    }
+  ],
+  "active_epoch": 210
+}
+|]
+
+transactionPoolUpdateExpected =
+  TransactionPoolUpdate
+    { _transactionPoolUpdateCertIndex = 0
+    , _transactionPoolUpdatePoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    , _transactionPoolUpdateVrfKey = "0b5245f9934ec2151116fb8ec00f35fd00e0aa3b075c4ed12cce440f999d8233"
+    , _transactionPoolUpdatePledge = 5000000000
+    , _transactionPoolUpdateMarginCost = 0.05
+    , _transactionPoolUpdateFixedCost = 340000000
+    , _transactionPoolUpdateRewardAccount = "stake1uxkptsa4lkr55jleztw43t37vgdn88l6ghclfwuxld2eykgpgvg3f"
+    , _transactionPoolUpdateOwners = [ "stake1u98nnlkvkk23vtvf9273uq7cph5ww6u2yq2389psuqet90sv4xv9v" ]
+    , _transactionPoolUpdateMetadata = Just samplePoolUpdateMetadata
+    , _transactionPoolUpdateRelays = [ samplePoolRelay ]
+    , _transactionPoolUpdateActiveEpoch = 210
+    }
+
+samplePoolUpdateMetadata :: PoolUpdateMetadata
+samplePoolUpdateMetadata =
+  PoolUpdateMetadata
+    { _poolUpdateMetadataUrl = Just "https://stakenuts.com/mainnet.json"
+    , _poolUpdateMetadataHash = Just "47c0c68cb57f4a5b4a87bad896fc274678e7aea98e200fa14a1cb40c0cab1d8c"
+    , _poolUpdateMetadataTicker = Just "NUTS"
+    , _poolUpdateMetadataName = Just "Stake Nuts"
+    , _poolUpdateMetadataDescription = Just "The best pool ever"
+    , _poolUpdateMetadataHomepage = Just "https://stakentus.com/"
+    }
+
+transactionPoolRetiringSample = [r|
+{
+  "cert_index": 0,
+  "pool_id": "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy",
+  "retiring_epoch": 216
+}
+|]
+
+transactionPoolRetiringExpected =
+  TransactionPoolRetiring
+    { _transactionPoolRetiringCertIndex = 0
+    , _transactionPoolRetiringPoolId = "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy"
+    , _transactionPoolRetiringRetiringEpoch = 216
+    }
+
+transactionMetaJSONSample = [r|
+[
+  {
+    "label": "1967",
+    "json_metadata": {
+      "metadata": "https://nut.link/metadata.json",
+      "hash": "6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af"
+    }
+  },
+  {
+    "label": "1968",
+    "json_metadata": {
+      "ADAUSD": [
+        {
+          "value": "0.15409850555139935",
+          "source": "ergoOracles"
+        }
+      ]
+    }
+  }
+]
+|]
+
+transactionMetaJSONExpected =
+    let oracleMeta val =
+          object [
+            "ADAUSD" .=
+              [ object [ "value" .= (val :: Text)
+                       , "source" .= ("ergoOracles" :: Text) ]
+              ]
+          ]
+    in
+    [ TransactionMetaJSON
+        "1967"
+        (Just $ object
+           [ "metadata" .= ("https://nut.link/metadata.json" :: Text)
+           , "hash" .= ("6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af" :: Text)
+           ])
+    , TransactionMetaJSON
+        "1968"
+        (Just $ oracleMeta "0.15409850555139935")
+    ]
+
+transactionMetaCBORSample = [r|
+{
+  "label": "1968",
+  "cbor_metadata": "\\xa100a16b436f6d62696e6174696f6e8601010101010c"
+}
+|]
+
+transactionMetaCBORExpected =
+    TransactionMetaCBOR
+      "1968"
+      (Just "\\xa100a16b436f6d62696e6174696f6e8601010101010c")
diff --git a/test/IPFS.hs b/test/IPFS.hs
new file mode 100644
--- /dev/null
+++ b/test/IPFS.hs
@@ -0,0 +1,77 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module IPFS
+  where
+
+import Data.Aeson (eitherDecode)
+import Data.Text (Text)
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_ipfs :: Spec
+spec_ipfs = do
+  it "parses add sample" $ do
+    eitherDecode pinAddSample
+    `shouldBe`
+    Right pinAddExpected
+
+  it "parses change sample" $ do
+    eitherDecode pinChangeSample
+    `shouldBe`
+    Right pinChangeExpected
+
+  it "parses pin sample" $ do
+    eitherDecode pinSample
+    `shouldBe`
+    Right pinExpected
+
+pinAddSample = [r|
+{
+  "name": "README.md",
+  "ipfs_hash": "QmZbHqiCxKEVX7QfijzJTkZiSi3WEVTcvANgNAWzDYgZDr",
+  "size": "125297"
+}
+|]
+
+pinAddExpected =
+  IPFSAdd
+    "README.md"
+    "QmZbHqiCxKEVX7QfijzJTkZiSi3WEVTcvANgNAWzDYgZDr"
+    125297
+
+pinChangeSample = [r|
+{
+  "ipfs_hash": "QmPojRfAXYAXV92Dof7gtSgaVuxEk64xx9CKvprqu9VwA8",
+  "state": "queued"
+}
+|]
+
+pinChangeExpected =
+  IPFSPinChange
+    "QmPojRfAXYAXV92Dof7gtSgaVuxEk64xx9CKvprqu9VwA8"
+    Queued
+
+pinSample = [r|
+{
+  "time_created": 1615551024,
+  "time_pinned": 1615551024,
+  "ipfs_hash": "QmdVMnULrY95mth2XkwjxDtMHvzuzmvUPTotKE1tgqKbCx",
+  "size": "1615551024",
+  "state": "pinned"
+}
+|]
+
+pinExpected =
+  IPFSPin
+    { _ipfsPinTimeCreated = 1615551024
+    , _ipfsPinTimePinned = 1615551024
+    , _ipfsPinIpfsHash = "QmdVMnULrY95mth2XkwjxDtMHvzuzmvUPTotKE1tgqKbCx"
+    , _ipfsPinSize = 1615551024
+    , _ipfsPinState = Pinned
+    }
diff --git a/test/NutLink.hs b/test/NutLink.hs
new file mode 100644
--- /dev/null
+++ b/test/NutLink.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module NutLink
+  where
+
+import Data.Aeson (Value (..), eitherDecode, object, (.=))
+import Data.Text (Text)
+import qualified Data.Vector
+import Test.Hspec
+import Test.Tasty.Hspec
+import Text.RawString.QQ
+
+import Blockfrost.Types
+
+spec_nutlink :: Spec
+spec_nutlink = do
+  it "parses nutlink address sample" $ do
+    eitherDecode nutlinkAddressSample
+    `shouldBe`
+    Right nutlinkAddressExpected
+
+  it "parses nutlink address tickers sample" $ do
+    eitherDecode nutlinkAddressTickersSample
+    `shouldBe`
+    Right nutlinkAddressTickersExpected
+
+  it "parses nutlink ticker sample" $ do
+    eitherDecode nutlinkTickerSample
+    `shouldBe`
+    Right nutlinkTickerExpected
+
+  it "parses nutlink ticker with address sample" $ do
+    eitherDecode nutlinkTickerWithAddressSample
+    `shouldBe`
+    Right nutlinkTickerWithAddressExpected
+
+nutlinkAddressSample = [r|
+{
+  "address": "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz",
+  "metadata_url": "https://nut.link/metadata.json",
+  "metadata_hash": "6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af",
+  "metadata": {}
+}
+|]
+
+nutlinkAddressExpected =
+  NutlinkAddress
+    { _nutlinkAddressAddress = "addr1qxqs59lphg8g6qndelq8xwqn60ag3aeyfcp33c2kdp46a09re5df3pzwwmyq946axfcejy5n4x0y99wqpgtp2gd0k09qsgy6pz"
+    , _nutlinkAddressMetadataUrl = "https://nut.link/metadata.json"
+    , _nutlinkAddressMetadataHash = "6bf124f217d0e5a0a8adb1dbd8540e1334280d49ab861127868339f43b3948af"
+    , _nutlinkAddressMetadata = pure $ object []
+    }
+
+nutlinkAddressTickersSample = [r|
+[
+  {
+    "name": "ADAUSD",
+    "count": 1980038,
+    "latest_block": 2657092
+  },
+  {
+    "name": "ADAEUR",
+    "count": 1980038,
+    "latest_block": 2657092
+  },
+  {
+    "name": "ADABTC",
+    "count": 1980038,
+    "latest_block": 2657092
+  }
+]
+|]
+
+nutlinkAddressTickersExpected =
+  [ NutlinkAddressTicker
+      { _nutlinkAddressTickerName = "ADAUSD"
+      , _nutlinkAddressTickerCount = 1980038
+      , _nutlinkAddressTickerLatestBlock = 2657092
+      }
+  , NutlinkAddressTicker
+      { _nutlinkAddressTickerName = "ADAEUR"
+      , _nutlinkAddressTickerCount = 1980038
+      , _nutlinkAddressTickerLatestBlock = 2657092
+      }
+  , NutlinkAddressTicker
+      { _nutlinkAddressTickerName = "ADABTC"
+      , _nutlinkAddressTickerCount = 1980038
+      , _nutlinkAddressTickerLatestBlock = 2657092
+      }
+  ]
+
+
+nutlinkTickerSample = [r|
+{
+  "tx_hash": "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b",
+  "block_height": 2657092,
+  "tx_index": 8,
+  "payload": [
+    {
+      "source": "coinGecko",
+      "value": "1.29"
+    },
+    {
+      "source": "cryptoCompare",
+      "value": "1.283"
+    }
+  ]
+}
+|]
+
+sampleArray = Array
+  $ Data.Vector.fromList
+  [ object [ "source" .= ("coinGecko" :: Text)
+           , "value" .= ("1.29" :: Text) ]
+  , object [ "source" .= ("cryptoCompare" :: Text),
+             "value" .= ("1.283" :: Text) ]
+  ]
+
+nutlinkTickerExpected =
+  NutlinkTicker
+    { _nutlinkTickerTxHash = "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b"
+    , _nutlinkTickerBlockHeight = 2657092
+    , _nutlinkTickerTxIndex = 8
+    , _nutlinkTickerPayload = sampleArray
+    }
+
+nutlinkTickerWithAddressSample = [r|
+{
+  "address": "addr_test1qpmtp5t0t5y6cqkaz7rfsyrx7mld77kpvksgkwm0p7en7qum7a589n30e80tclzrrnj8qr4qvzj6al0vpgtnmrkkksnqd8upj0",
+  "tx_hash": "e8073fd5318ff43eca18a852527166aa8008bee9ee9e891f585612b7e4ba700b",
+  "block_height": 2657092,
+  "tx_index": 8,
+  "payload": [
+    {
+      "source": "coinGecko",
+      "value": "1.29"
+    },
+    {
+      "source": "cryptoCompare",
+      "value": "1.283"
+    }
+  ]
+}
+|]
+
+nutlinkTickerWithAddressExpected :: (Address, NutlinkTicker)
+nutlinkTickerWithAddressExpected =
+  ("addr_test1qpmtp5t0t5y6cqkaz7rfsyrx7mld77kpvksgkwm0p7en7qum7a589n30e80tclzrrnj8qr4qvzj6al0vpgtnmrkkksnqd8upj0"
+  , nutlinkTickerExpected)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF tasty-discover #-}
