diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# CHANGELOG
+
+## v0.1.0.0
+
+* Initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Pavan Rikhi (c) 2021
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Pavan Rikhi nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# cointracking-imports
+
+[![cointracking-imports Build Status](https://github.com/prikhi/cointracking-imports/actions/workflows/main.yml/badge.svg)](https://github.com/prikhi/cointracking-imports/actions/workflows/main.yml)
+
+
+A Haskell library for generating CSV & XLSX files for importing into
+[CoinTracking][cointracking]. Build a `[CTImportData]` & write to a file with
+`writeImportDataToFile`.
+
+Requires [`stack`][get-stack].
+
+[cointracking]: https://cointracking.info/
+[get-stack]: https://docs.haskellstack.org/en/stable/README/
+
+
+## Build
+
+You can build the project with stack:
+
+```sh
+stack build
+```
+
+For development, you can enable fast builds with file-watching,
+documentation-building, & test-running:
+
+```sh
+stack test --haddock --fast --file-watch --pedantic
+```
+
+To build & open the documentation, run:
+
+```sh
+stack haddock --open cointracking-imports
+```
+
+
+## LICENSE
+
+BSD-3
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/cointracking-imports.cabal b/cointracking-imports.cabal
new file mode 100644
--- /dev/null
+++ b/cointracking-imports.cabal
@@ -0,0 +1,106 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.34.4.
+--
+-- see: https://github.com/sol/hpack
+
+name:           cointracking-imports
+version:        0.1.0.0
+synopsis:       Generate CSV & XLSX files for importing into CoinTracking.
+description:    @cointracking-imports@ is a library for generating CSV & XLSX files to use
+                with <https://cointracking.info CoinTracking>'s Import feature. You
+                construct a list of @CTImportData@, then feed those to our CSV/XLSX
+                generating functions.
+                .
+                E.g., if you generate a list of your BNB staking rewards, you can use this
+                library build a CSV file to import those rewards into CoinTracking.
+category:       Web, Finance
+homepage:       https://github.com/prikhi/cointracking-imports#readme
+bug-reports:    https://github.com/prikhi/cointracking-imports/issues
+author:         Pavan Rikhi
+maintainer:     pavan.rikhi@gmail.com
+copyright:      2021 Pavan Rikhi
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://github.com/prikhi/cointracking-imports
+
+library
+  exposed-modules:
+      Web.CoinTracking.Imports
+      Web.CoinTracking.Imports.Types
+  other-modules:
+      Paths_cointracking_imports
+  hs-source-dirs:
+      src
+  default-extensions:
+      DeriveGeneric
+      LambdaCase
+      NamedFieldPuns
+      OverloadedStrings
+      TupleSections
+      TypeApplications
+      TypeOperators
+      ViewPatterns
+  ghc-options: -Wall
+  build-depends:
+      base >=4.3 && <5
+    , base-compat-batteries
+    , bytestring <1
+    , cassava <1
+    , filepath ==1.*
+    , lens >=3.0.4 && <6
+    , scientific <1
+    , text <2
+    , time <2
+    , xlsx <1
+  if impl(ghc >= 8.0)
+    ghc-options: -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
+  else
+    ghc-options: -fwarn-incomplete-record-updates -fwarn-incomplete-uni-patterns
+  default-language: Haskell2010
+
+test-suite cointracking-imports-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_cointracking_imports
+  hs-source-dirs:
+      tests
+  default-extensions:
+      DeriveGeneric
+      LambdaCase
+      NamedFieldPuns
+      OverloadedStrings
+      TupleSections
+      TypeApplications
+      TypeOperators
+      ViewPatterns
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts "-N -T"
+  build-depends:
+      base >=4.3 && <5
+    , base-compat-batteries
+    , bytestring <1
+    , cassava <1
+    , cointracking-imports
+    , filepath ==1.*
+    , hedgehog
+    , lens >=3.0.4 && <6
+    , scientific <1
+    , tasty
+    , tasty-hedgehog
+    , tasty-hunit
+    , text <2
+    , time <2
+    , xlsx <1
+  if impl(ghc >= 8.0)
+    ghc-options: -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
+  else
+    ghc-options: -fwarn-incomplete-record-updates -fwarn-incomplete-uni-patterns
+  default-language: Haskell2010
diff --git a/src/Web/CoinTracking/Imports.hs b/src/Web/CoinTracking/Imports.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/CoinTracking/Imports.hs
@@ -0,0 +1,165 @@
+{-# LANGUAGE RecordWildCards #-}
+{-| Generate CSV & XLSX files to use with CoinTracking's import feature.
+
+-}
+module Web.CoinTracking.Imports
+    ( writeImportDataToFile
+      -- * Types
+    , module Web.CoinTracking.Imports.Types
+      -- * CSV Import Files
+    , coinTrackingCsvImport
+    , headerRow
+    , csvEncodingOptions
+      -- * XLSX Import Files
+    , coinTrackingXlsxImport
+    , writeXlsxHeader
+    , writeXlsxRow
+    ) where
+
+import           Codec.Xlsx                     ( CellValue(..)
+                                                , DateBase(DateBase1900)
+                                                , Worksheet
+                                                , atSheet
+                                                , cellValueAt
+                                                , dateToNumber
+                                                , def
+                                                , fromXlsx
+                                                )
+import           Control.Lens                   ( (.~)
+                                                , (?~)
+                                                )
+import           Data.Char                      ( toLower )
+import           Data.Csv                       ( EncodeOptions(..)
+                                                , defaultEncodeOptions
+                                                , encodeWith
+                                                )
+import           Data.Foldable                  ( foldl' )
+import           Data.Function                  ( (&) )
+import           Data.Scientific                ( toRealFloat )
+import           Data.Time                      ( zonedTimeToUTC )
+import           Data.Time.Clock.POSIX          ( POSIXTime
+                                                , getPOSIXTime
+                                                )
+import           System.FilePath                ( takeExtension )
+
+import           Web.CoinTracking.Imports.Types
+
+import qualified Data.ByteString.Lazy          as LBS
+import qualified Data.ByteString.Lazy.Char8    as LBC
+import qualified Data.Text                     as T
+
+
+-- | Write the given data to a file. If the file extension is @.xlsx@ or
+-- @.xls@, we write a spreadsheet. Otherwise we write a CSV.
+writeImportDataToFile :: FilePath -> [CTImportData] -> IO ()
+writeImportDataToFile file xs = do
+    currentTime <- getPOSIXTime
+    let extension = takeExtension file
+        output    = if map toLower extension `elem` [".xlsx", ".xls"]
+            then coinTrackingXlsxImport currentTime xs
+            else coinTrackingCsvImport xs
+    LBC.writeFile file output
+
+
+-- CSVs
+
+-- | Generate the CoinTracking CSV Import for the data, prepended by
+-- a header row.
+coinTrackingCsvImport :: [CTImportData] -> LBS.ByteString
+coinTrackingCsvImport = (headerRow <>) . encodeWith csvEncodingOptions
+
+-- | The CSV header row to prepend to the generated output.
+headerRow :: LBS.ByteString
+headerRow = encodeWith
+    csvEncodingOptions
+    [ [ "Type" :: T.Text
+      , "Buy"
+      , "Cur."
+      , "Sell"
+      , "Cur."
+      , "Fee"
+      , "Cur."
+      , "Exchange"
+      , "Trade-Group"
+      , "Comment"
+      , "Date"
+      , "Tx-ID"
+      , "Buy Value in your Account Currency"
+      , "Sell Value in your Account Currency"
+      ]
+    ]
+
+-- | 'defaultEncodeOptions', but with newline-only line endings.
+csvEncodingOptions :: EncodeOptions
+csvEncodingOptions = defaultEncodeOptions { encUseCrLf = False }
+
+
+-- XLSXs
+
+-- | Generate an XLSX file containing the expected headers rows and the
+-- import data.
+coinTrackingXlsxImport
+    :: POSIXTime
+    -- ^ Creation time to embed in the spreadsheet.
+    -> [CTImportData]
+    -> LBS.ByteString
+coinTrackingXlsxImport createdTime rows =
+    let sheet = ixFoldl
+            (\sheet_ rowNum row -> writeXlsxRow sheet_ (rowNum + 3) row)
+            (writeXlsxHeader def)
+            rows
+        book = def & atSheet "Sheet1" ?~ sheet
+    in  fromXlsx createdTime book
+  where
+    -- | Indexed fold from the left.
+    ixFoldl :: (b -> Int -> a -> b) -> b -> [a] -> b
+    ixFoldl f initial =
+        fst . foldl' (\(b, i) a -> (f b i a, i + 1)) (initial, 0)
+
+-- | Write the standard CoinTracking header to the first two rows of the
+-- worksheet.
+writeXlsxHeader :: Worksheet -> Worksheet
+writeXlsxHeader sheet =
+    sheet
+        &  cellValueAt (1, 1)
+        ?~ CellText
+               "CoinTracking Excel Import data (see docs: https://cointracking.info/import/import_xls/)"
+        &  writeColumn 1  "Type"
+        &  writeColumn 2  "Buy Amount"
+        &  writeColumn 3  "Buy Cur."
+        &  writeColumn 4  "Sell Amount"
+        &  writeColumn 5  "Sell Cur."
+        &  writeColumn 6  "Feel Amount"
+        &  writeColumn 7  "Fee Cur."
+        &  writeColumn 8  "Exchange"
+        &  writeColumn 9  "Trade Group"
+        &  writeColumn 10 "Comment"
+        &  writeColumn 11 "Date"
+  where
+    writeColumn :: Int -> T.Text -> Worksheet -> Worksheet
+    writeColumn c t s = s & cellValueAt (2, c) ?~ CellText t
+
+
+-- | Write a 'CTImportData' to the given row(1-indexed) of the worksheet.
+writeXlsxRow :: Worksheet -> Int -> CTImportData -> Worksheet
+writeXlsxRow sheet row CTImportData {..} =
+    sheet
+        & (cellValueAt (row, 1) ?~ CellText (renderTransactionType ctidType))
+        & (cellValueAt (row, 2) .~ renderAmount ctidBuy)
+        & (cellValueAt (row, 3) .~ renderCurrency ctidBuy)
+        & (cellValueAt (row, 4) .~ renderAmount ctidSell)
+        & (cellValueAt (row, 5) .~ renderCurrency ctidSell)
+        & (cellValueAt (row, 6) .~ renderAmount ctidFee)
+        & (cellValueAt (row, 7) .~ renderCurrency ctidFee)
+        & (cellValueAt (row, 8) ?~ CellText ctidExchange)
+        & (cellValueAt (row, 9) ?~ CellText ctidGroup)
+        & (cellValueAt (row, 10) ?~ CellText ctidComment)
+        & (cellValueAt (row, 11) ?~ renderedDate)
+  where
+    renderAmount :: Maybe Amount -> Maybe CellValue
+    renderAmount = fmap (CellDouble . toRealFloat . aAmount)
+    renderCurrency :: Maybe Amount -> Maybe CellValue
+    renderCurrency = fmap (CellText . cTicker . aCurrency)
+    renderedDate :: CellValue
+    renderedDate =
+        CellDouble $ dateToNumber DateBase1900 $ zonedTimeToUTC ctidDate
diff --git a/src/Web/CoinTracking/Imports/Types.hs b/src/Web/CoinTracking/Imports/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Web/CoinTracking/Imports/Types.hs
@@ -0,0 +1,173 @@
+{-| Type definitions, instances, & utility functions for CoinTracking
+imports.
+
+-}
+{-# LANGUAGE RecordWildCards #-}
+module Web.CoinTracking.Imports.Types
+    ( CTImportData(..)
+    , CTTransactionType(..)
+    , renderTransactionType
+    , Amount(..)
+    , Currency(..)
+    ) where
+
+import           Data.Csv                       ( Field
+                                                , ToField(..)
+                                                , ToRecord(..)
+                                                , record
+                                                )
+import           Data.Scientific                ( FPFormat(Fixed)
+                                                , Scientific
+                                                , formatScientific
+                                                )
+import           Data.String                    ( IsString )
+import           Data.Time                      ( ZonedTime
+                                                , defaultTimeLocale
+                                                , formatTime
+                                                )
+import           GHC.Generics                   ( Generic )
+
+import qualified Data.Text                     as T
+
+-- | Represents a single row in an export.
+data CTImportData = CTImportData
+    { ctidType      :: CTTransactionType
+    , ctidBuy       :: Maybe Amount
+    , ctidSell      :: Maybe Amount
+    , ctidFee       :: Maybe Amount
+    , ctidExchange  :: T.Text
+    , ctidGroup     :: T.Text
+    , ctidComment   :: T.Text
+    , ctidDate      :: ZonedTime
+    , ctidTradeId   :: T.Text
+    , ctidBuyValue  :: Maybe Amount
+    , ctidSellValue :: Maybe Amount
+    }
+    deriving (Show, Read, Generic)
+
+instance ToRecord CTImportData where
+    toRecord CTImportData {..} = record
+        [ toField ctidType
+        , orBlank renderAmount   ctidBuy
+        , orBlank renderCurrency ctidBuy
+        , orBlank renderAmount   ctidSell
+        , orBlank renderCurrency ctidSell
+        , orBlank renderAmount   ctidFee
+        , orBlank renderCurrency ctidFee
+        , toField ctidExchange
+        , toField ctidGroup
+        , toField ctidComment
+        , toField $ formatTime defaultTimeLocale "%F %T" ctidDate
+        , toField ctidTradeId
+        , orBlank renderAmount ctidBuyValue
+        , orBlank renderAmount ctidSellValue
+        ]
+      where
+        orBlank :: (a -> Field) -> Maybe a -> Field
+        orBlank = maybe ""
+        renderAmount :: Amount -> Field
+        renderAmount Amount { aCurrency = Currency {..}, ..} =
+            toField $ formatScientific Fixed (Just cPrecision) aAmount
+        renderCurrency :: Amount -> Field
+        renderCurrency = toField . cTicker . aCurrency
+
+
+-- | An amount & currency specification.
+data Amount = Amount
+    { aAmount   :: Scientific
+    -- ^ The total amount.
+    , aCurrency :: Currency
+    -- ^ The currency symbol & decimal-precision.
+    }
+    deriving (Show, Read, Eq)
+
+-- | A pair containing a currency symbol & an amount.
+data Currency = Currency
+    { cPrecision :: Int
+    -- ^ The number of decimals places of precision.
+    , cTicker    :: T.Text
+    -- ^ The ticker symbol
+    }
+    deriving (Show, Read, Eq, Generic)
+
+
+-- | Possible types for an imported transaction.
+data CTTransactionType
+    = Trade
+    | Deposit
+    | Withdrawal
+    | Income
+    | Mining
+    | GiftTipIn
+    | Spend
+    | Donation
+    | GiftOut
+    | Stolen
+    | Lost
+    | Airdrop
+    | Staking
+    | Masternode
+    | Minting
+    | DividendsIncome
+    | LendingIncome
+    | InterestIncome
+    | RewardBonus
+    | MiningCommercial
+    | MarginProfit
+    | DerivativesFuturesProfit
+    | OtherIncome
+    | IncomeNonTaxable
+    | OtherIncomeNonTaxable
+    | MarginLoss
+    | MarginFee
+    | BorrowingFee
+    | SettlementFee
+    | DerivativesFuturesLoss
+    | OtherFee
+    | OtherExpense
+    | ExpenseNonTaxable
+    | MarginTrade
+    | DerivativesFuturesTrade
+    deriving (Show, Read, Eq, Ord, Bounded, Enum, Generic)
+
+instance ToField CTTransactionType where
+    toField = renderTransactionType
+
+-- | Render the 'CTTransactionType' as CoinTracking displays/expects it.
+renderTransactionType :: (IsString a) => CTTransactionType -> a
+renderTransactionType = \case
+    Trade                    -> "Trade"
+    Deposit                  -> "Deposit"
+    Withdrawal               -> "Withdrawal"
+    Income                   -> "Income"
+    Mining                   -> "Mining"
+    GiftTipIn                -> "Gift/Tip(In)"
+    Spend                    -> "Spend"
+    Donation                 -> "Donation"
+    GiftOut                  -> "Gift(Out)"
+    Stolen                   -> "Stolen"
+    Lost                     -> "Lost"
+    Airdrop                  -> "Airdrop"
+    Staking                  -> "Staking"
+    Masternode               -> "Masternode"
+    Minting                  -> "Minting"
+    DividendsIncome          -> "Dividends Income"
+    LendingIncome            -> "Lending Income"
+    InterestIncome           -> "Interest Income"
+    RewardBonus              -> "Reward / Bonus"
+    MiningCommercial         -> "Mining (commercial)"
+    MarginProfit             -> "Margin Profit"
+    DerivativesFuturesProfit -> "Derivatives / Futures Profit"
+    OtherIncome              -> "Other Income"
+    IncomeNonTaxable         -> "Income (non taxable)"
+    OtherIncomeNonTaxable    -> "Other Income (non taxable)"
+    MarginLoss               -> "Margin Loss"
+    MarginFee                -> "Margin Fee"
+    BorrowingFee             -> "Borrowing Fee"
+    SettlementFee            -> "Settlement Fee"
+    DerivativesFuturesLoss   -> "Derivatives / Futures Loss"
+    OtherFee                 -> "Other Fee"
+    OtherExpense             -> "Other Expense"
+    ExpenseNonTaxable        -> "Expense (non taxable)"
+    MarginTrade              -> "Margin Trade"
+    DerivativesFuturesTrade  -> "Derivatives / Futures Trade"
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,34 @@
+import           Hedgehog
+import           Test.Tasty
+import           Test.Tasty.HUnit
+import           Test.Tasty.Hedgehog
+
+import qualified Hedgehog.Gen                  as Gen
+import qualified Hedgehog.Range                as Range
+
+
+main :: IO ()
+main = defaultMain tests
+
+
+tests :: TestTree
+tests = testGroup "Tests" [unitTests, properties]
+
+
+unitTests :: TestTree
+unitTests = testGroup "Unit Tests" [testCase "2+2 = 4" testAddition]
+  where
+    testAddition :: Assertion
+    testAddition = (2 + 2) @?= (4 :: Integer)
+
+
+properties :: TestTree
+properties = testGroup
+    "Properties"
+    [testProperty "Addition is Communative" testAdditionCommunative]
+  where
+    testAdditionCommunative :: Property
+    testAdditionCommunative = property $ do
+        let genInt = Gen.int $ Range.linear 0 9001
+        (a, b) <- forAll $ (,) <$> genInt <*> genInt
+        (a + b) === (b + a)
