diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,17 @@
+0.1.0.0, released 2025-09-04
+    Initial version (work in progress)
+
+0.2.0.0, released 2025-09-12
+    Removed umbrella project (this library is now standalone)
+
+0.2.1.0, released 2025-09-12
+    Added release.sh (required for release build)
+
+0.3.0.0, released 2025-09-12
+    Updated version in package.yaml
+
+0.4.0.0, released 2025-09-17
+    Updated version reminder
+
+0.4.1.0, released 2025-12-02
+    Publish to Hackage
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,217 @@
+Copyright A.H.M. (Mari) Donkers 2025.
+
+Licensed under either of:
+
+* BSD-3-Clause license (https://opensource.org/licenses/BSD-3-Clause)
+* Apache License, version 2.0 (https://opensource.org/licenses/Apache-2.0)
+
+As a user, you may use this code under either license, at your option.
+
+
+---------------------------------------------------------------------
+BSD 3-Clause License
+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 Neil Mitchell 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.
+
+
+---------------------------------------------------------------------
+Apache License
+Version 2.0, January 2004
+<http://www.apache.org/licenses/>
+
+Terms and Conditions for use, reproduction, and distribution
+1. Definitions
+"License" shall mean the terms and conditions for use, reproduction,
+and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the
+copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all
+other entities that control, are controlled by, or are under common
+control with that entity. For the purposes of this definition,
+"control" means (i) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or
+otherwise, or (ii) ownership of fifty percent (50%) or more of the
+outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications,
+including but not limited to software source code, documentation
+source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical
+transformation or translation of a Source form, including but not
+limited to compiled object code, generated documentation, and
+conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object
+form, made available under the License, as indicated by a copyright
+notice that is included in or attached to the work).
+
+"Derivative Works" shall mean any work, whether in Source or Object
+form, that is based on (or derived from) the Work and for which the
+editorial revisions, annotations, elaborations, or other
+modifications represent, as a whole, an original work of authorship.
+For the purposes of this License, Derivative Works shall not include
+works that remain separable from, or merely link (or bind by name) to
+the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the
+original version of the Work and any modifications or additions to
+that Work or Derivative Works thereof, that is intentionally
+submitted to Licensor for inclusion in the Work by the copyright
+owner or by an individual or Legal Entity authorized to submit on
+behalf of the copyright owner. For the purposes of this definition,
+"submitted" means any form of electronic, verbal, or written
+communication sent to the Licensor or its representatives, including
+but not limited to communication on electronic mailing lists, source
+code control systems, and issue tracking systems that are managed by,
+or on behalf of, the Licensor for the purpose of discussing and
+improving the Work, but excluding communication that is conspicuously
+marked or otherwise designated in writing by the copyright owner as
+"Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity
+on behalf of whom a Contribution has been received by Licensor and
+subsequently incorporated within the Work.
+
+2. Grant of Copyright License
+Subject to the terms and conditions of this License, each Contributor
+hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable copyright license to reproduce,
+prepare Derivative Works of, publicly display, publicly perform,
+sublicense, and distribute the Work and such Derivative Works in
+Source or Object form.
+
+3. Grant of Patent License
+Subject to the terms and conditions of this License, each Contributor
+hereby grants to You a perpetual, worldwide, non-exclusive,
+no-charge, royalty-free, irrevocable (except as stated in this
+section) patent license to make, have made, use, offer to sell, sell,
+import, and otherwise transfer the Work, where such license applies
+only to those patent claims licensable by such Contributor that are
+necessarily infringed by their Contribution(s) alone or by
+combination of their Contribution(s) with the Work to which such
+Contribution(s) was submitted. If You institute patent litigation
+against any entity (including a cross-claim or counterclaim in a
+lawsuit) alleging that the Work or a Contribution incorporated within
+the Work constitutes direct or contributory patent infringement, then
+any patent licenses granted to You under this License for that Work
+shall terminate as of the date such litigation is filed.
+
+4. Redistribution
+You may reproduce and distribute copies of the Work or Derivative
+Works thereof in any medium, with or without modifications, and in
+Source or Object form, provided that You meet the following
+conditions:
+
+(a) You must give any other recipients of the Work or Derivative
+Works a copy of this License; and
+(b) You must cause any modified files to carry prominent notices
+stating that You changed the files; and
+(c) You must retain, in the Source form of any Derivative Works that
+You distribute, all copyright, patent, trademark, and attribution
+notices from the Source form of the Work, excluding those notices
+that do not pertain to any part of the Derivative Works; and
+(d) If the Work includes a "NOTICE" text file as part of its
+distribution, then any Derivative Works that You distribute must
+include a readable copy of the attribution notices contained within
+such NOTICE file, excluding those notices that do not pertain to any
+part of the Derivative Works, in at least one of the following
+places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if
+provided along with the Derivative Works; or, within a display
+generated by the Derivative Works, if and wherever such third-party
+notices normally appear. The contents of the NOTICE file are for
+informational purposes only and do not modify the License. You may
+add Your own attribution notices within Derivative Works that You
+distribute, alongside or as an addendum to the NOTICE text from the
+Work, provided that such additional attribution notices cannot be
+construed as modifying the License.
+You may add Your own copyright statement to Your modifications and
+may provide additional or different license terms and conditions for
+use, reproduction, or distribution of Your modifications, or for any
+such Derivative Works as a whole, provided Your use, reproduction,
+and distribution of the Work otherwise complies with the conditions
+stated in this License.
+
+5. Submission of Contributions
+Unless You explicitly state otherwise, any Contribution intentionally
+submitted for inclusion in the Work by You to the Licensor shall be
+under the terms and conditions of this License, without any
+additional terms or conditions. Notwithstanding the above, nothing
+herein shall supersede or modify the terms of any separate license
+agreement you may have executed with Licensor regarding such
+Contributions.
+
+6. Trademarks
+This License does not grant permission to use the trade names,
+trademarks, service marks, or product names of the Licensor, except
+as required for reasonable and customary use in describing the origin
+of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty
+Unless required by applicable law or agreed to in writing, Licensor
+provides the Work (and each Contributor provides its Contributions)
+on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+either express or implied, including, without limitation, any
+warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY,
+or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for
+determining the appropriateness of using or redistributing the Work
+and assume any risks associated with Your exercise of permissions
+under this License.
+
+8. Limitation of Liability
+In no event and under no legal theory, whether in tort (including
+negligence), contract, or otherwise, unless required by applicable
+law (such as deliberate and grossly negligent acts) or agreed to in
+writing, shall any Contributor be liable to You for damages,
+including any direct, indirect, special, incidental, or consequential
+damages of any character arising as a result of this License or out
+of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or
+malfunction, or any and all other commercial damages or losses), even
+if such Contributor has been advised of the possibility of such
+damages.
+
+9. Accepting Warranty or Additional Liability
+While redistributing the Work or Derivative Works thereof, You may
+choose to offer, and charge a fee for, acceptance of support,
+warranty, indemnity, or other liability obligations and/or rights
+consistent with this License. However, in accepting such obligations,
+You may act only on Your own behalf and on Your sole responsibility,
+not on behalf of any other Contributor, and only if You agree to
+indemnify, defend, and hold each Contributor harmless for any
+liability incurred by, or claims asserted against, such Contributor
+by reason of your accepting any such warranty or additional
+liability.
diff --git a/Makefile b/Makefile
new file mode 100644
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,70 @@
+PROJECT=haskell-opentimestamps-client
+
+.PHONY: help
+help: ## print make targets
+	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+package: package.yaml
+	hpack
+
+.PHONY: freeze
+freeze:  ## Freeze dependencies
+	cabal freeze
+
+.PHONY: clean
+clean:   ## Clean project
+	cabal clean
+
+.PHONY: build
+build:  package ## Build project
+	cabal build
+
+.PHONY: repl
+repl:   package ## Repl with project pre-loaded
+	cabal repl $(PROJECT)
+
+.PHONY: format
+format:   ## Format Haskell source code
+	fourmolu -i `ag -l --haskell`
+
+.PHONY: lint
+lint:   ## Lint Haskell source code
+	hlint `ag -l --haskell`
+
+.PHONY: ls
+ls:     ## get path of executable
+	cabal ls-bin $(PROJECT)
+
+.PHONY: run
+run:    package ## run the project with cabal
+	cabal run
+
+.PHONY: release
+release:  ## release build (no debug symbols, etc).
+	cabal build --enable-executable-stripping all
+	cabal list-bin "$(PROJECT)-exe"
+
+.PHONY: check
+check:  ## runs cabal check
+	cabal check
+
+.PHONY: sdist
+sdist:  ## generate a distribution tarball
+	cabal sdist
+
+# HOOGLE="http://localhost:4041" emacs &
+.PHONY: edit
+edit:   ## Start editor
+	emacs &
+
+# https://blog.ssanj.net/posts/2019-10-19-running-hoogle-locally-for-haskell-dev.html
+.PHONY: hoogledb
+hoogledb: ## Create local Hoogle database
+	cabal-hoogle generate
+	# stack hoogle -- generate --local
+
+# https://blog.ssanj.net/posts/2019-10-19-running-hoogle-locally-for-haskell-dev.html
+.PHONY: hoogle
+hoogle: ## Run local Hoogle server
+	cabal-hoogle run -- server --local --port 4041
+	# stack hoogle -- server --local --port=4041
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,30 @@
+This is a Haskell client for OpenTimestamps, providing a command-line utility to interact with the OpenTimestamps network. It's on Hackage here: [haskell-opentimestamps-client](https://hackage.haskell.org/package/haskell-opentimestamps-client). The client program allows users to:
+*   **Info:** Display information about a timestamp file.
+*   **Stamp:** Create OpenTimestamps for files.
+*   **Prune:** Prune a timestamp file.
+*   **Upgrade:** Upgrade remote calendar timestamps to be locally verifiable.
+*   **Verify:** Verify existing OpenTimestamps files.
+
+The client uses the `haskell-opentimestamps` library for core OpenTimestamps stuff.
+
+The project has a number of dependencies, including `cryptonite` for cryptographic operations, `http-client` for making requests to calendar servers and `haskoin-code` and `bitcoin-rpc` to make RPC-calls to a locally running 'blocksonly' pruned Bitcoin node, which is required for **Verification**.
+
+Also you will need to have a `libsecp256k1` package (Debian: `libsecp256k1-dev`) installed on your Linux distribution (beware: it needs to be < 0.7.x because of deprecated functions in `0.7.x`).
+
+To build an executable:
+
+```sh
+make release
+```
+
+Updating version (development reminder):
+- `version: #.#.#.#` in `package.yaml`
+- run `hpack` to regenerate the `.cabal` file
+- Add the version to `CHANGELOG.md`
+- Commit the new version.
+- `git tag -a #.#.#.#` to add a git tag
+- `git push yourorigin #.#.#.#` to push the git tag
+
+**Work In Progress**...
+
+There is also a Haskell library for working with OpenTimestamps. It's on Hackage here: [haskell-opentimestamps](https://hackage.haskell.org/package/haskell-opentimestamps).
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,149 @@
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Main (main) where
+
+import qualified OpenTimestampsClient as OTC
+import Options.Applicative
+  ( Parser
+  , command
+  , execParser
+  , fullDesc
+  , help
+  , helper
+  , info
+  , long
+  , metavar
+  , progDesc
+  , short
+  , some
+  , strArgument
+  , subparser
+  , switch
+  , (<**>)
+  )
+
+calendarUrls :: [String]
+calendarUrls =
+  [ "https://a.pool.opentimestamps.org"
+  , "https://b.pool.opentimestamps.org"
+  , "https://a.pool.eternitywall.com"
+  , "https://ots.btc.catallaxy.com"
+  ]
+
+data GlobalOptions where
+  GlobalOptions :: {globalVerbose :: Bool} -> GlobalOptions
+
+verboseOption :: Parser Bool
+verboseOption =
+  switch
+    ( long "verbose"
+        <> short 'v'
+        <> help "Enable verbose output"
+    )
+
+globalOptionsParser :: Parser GlobalOptions
+globalOptionsParser =
+  GlobalOptions
+    <$> verboseOption
+
+-- TODO Add GitExtract (?) to Extract timestamp for a single file from a timestamp git commit
+
+data Command where
+  Info :: FilePath -> Command
+  Prune :: FilePath -> Command
+  Stamp :: [FilePath] -> Command
+  Upgrade :: FilePath -> Command
+  Verify :: FilePath -> Command
+
+infoParser :: Parser Command
+infoParser =
+  Info
+    <$> strArgument
+      ( metavar "FILE"
+          <> help "Timestamp file (.ots)"
+      )
+
+pruneParser :: Parser Command
+pruneParser =
+  Prune
+    <$> strArgument
+      ( metavar "FILE"
+          <> help "[TODO] Timestamp file (.ots)"
+      )
+
+stampParser :: Parser Command
+stampParser =
+  Stamp
+    <$> some
+      ( strArgument
+          ( metavar "FILE"
+              <> help "File(s) to timestamp"
+          )
+      )
+
+upgradeParser :: Parser Command
+upgradeParser =
+  Upgrade
+    <$> strArgument
+      ( metavar "FILE"
+          <> help "[TODO] Timestamp file (.ots)"
+      )
+
+verifyParser :: Parser Command
+verifyParser =
+  Verify
+    <$> strArgument
+      ( metavar "FILE"
+          <> help "Timestamp file (.ots)"
+      )
+
+commandParser :: Parser Command
+commandParser =
+  subparser
+    ( command
+        "info"
+        (info infoParser (progDesc "Show information on a timestamp"))
+        <> command
+          "prune"
+          (info pruneParser (progDesc "Prune timestamp"))
+        <> command
+          "stamp"
+          (info stampParser (progDesc "Timestamp file(s)"))
+        <> command
+          "upgrade"
+          ( info
+              upgradeParser
+              (progDesc "Upgrade remote calendar timestamps to be locally verifiable")
+          )
+        <> command
+          "verify"
+          (info verifyParser (progDesc "Verify a timestamp"))
+    )
+
+data AppParser where
+  AppParser :: GlobalOptions -> Command -> AppParser
+
+appParser :: Parser AppParser
+appParser = AppParser <$> globalOptionsParser <*> commandParser
+
+-- | Main entry point of the application.
+main :: IO ()
+main = do
+  AppParser gOpts cmd <-
+    execParser (info (appParser <**> helper) (fullDesc <> progDesc "OpenTimestamps client"))
+  run gOpts cmd
+  where
+    run :: GlobalOptions -> Command -> IO ()
+    run _gOpts (Info path) = do
+      -- putStrLn $ "Getting info for: " ++ path
+      OTC.info path
+    run gOpts (Prune path) = do
+      OTC.prune (globalVerbose gOpts) path [""] [""] -- TODO verify- and discard args
+    run gOpts (Stamp paths) = do
+      OTC.stamp (globalVerbose gOpts) paths calendarUrls
+    run gOpts (Upgrade path) = do
+      OTC.upgrade (globalVerbose gOpts) path
+    run gOpts (Verify path) = do
+      OTC.verify (globalVerbose gOpts) path
diff --git a/cabal.project b/cabal.project
new file mode 100644
--- /dev/null
+++ b/cabal.project
@@ -0,0 +1,21 @@
+packages: .
+          
+source-repository-package
+  type: git
+  location: https://github.com/bitnomial/bitcoin-compact-filters
+  -- branch: master
+  tag: 7ae4d9bec8a5275db579e34e3cadb86c0f0461e9
+
+source-repository-package
+  type: git
+  location: https://github.com/bitnomial/bitcoind-rpc
+  -- branch: master
+  tag: 16d664e05c232800aa6bc4f63c372c6f3e0e5d39
+  subdir: bitcoind-rpc
+
+source-repository-package
+  type: git
+  location: https://codeberg.org/photonsphere/haskell-opentimestamps.git
+  --location: /absolutepathonyourcomputerhere/haskell-opentimestamps
+  branch: master
+  --tag: 0.1.0.0
diff --git a/haskell-opentimestamps-client.cabal b/haskell-opentimestamps-client.cabal
new file mode 100644
--- /dev/null
+++ b/haskell-opentimestamps-client.cabal
@@ -0,0 +1,66 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.38.2.
+--
+-- see: https://github.com/sol/hpack
+
+name:           haskell-opentimestamps-client
+version:        0.4.1.0
+synopsis:       Haskell implementation of a basic OpenTimestamps client
+description:    Please see the README on Codeberg at <https://codeberg.org/photonsphere/haskell-opentimestamps-client#readme>
+category:       Bitcoin, Finance, Network, Cryptography
+author:         A.H.M. (Mari) Donkers
+maintainer:     a.h.m.donkers@photonsphere.org
+copyright:      2025 A.H.M. (Mari) Donkers
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    CHANGELOG.md
+    cabal.project
+    package.yaml
+    Makefile
+
+source-repository head
+  type: git
+  location: https://codeberg.org/photonsphere/haskell-opentimestamps-client
+
+library
+  exposed-modules:
+      OpenTimestampsClient
+  other-modules:
+      Paths_haskell_opentimestamps_client
+  hs-source-dirs:
+      src
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
+  build-depends:
+      base >=4.7 && <5
+    , base16 >=0.1 && <2.0
+    , bytestring >=0.10 && <0.13
+    , containers >=0.5 && <0.8
+    , cryptonite >=0.25 && <0.35
+    , directory >=1.2 && <1.4
+    , filepath >=1.3 && <1.5
+    , haskell-opentimestamps >=0.5.5.0 && <1.0
+    , memory >=0.14 && <0.19
+    , temporary >=1.2 && <1.4
+    , text >=1.2 && <2.2
+    , time >=1.4 && <1.13
+  default-language: Haskell2010
+
+executable haskell-opentimestamps-client-exe
+  main-is: Main.hs
+  other-modules:
+      Paths_haskell_opentimestamps_client
+  hs-source-dirs:
+      app
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , bytestring >=0.10 && <0.13
+    , cryptonite >=0.25 && <0.35
+    , haskell-opentimestamps-client
+    , memory >=0.14 && <0.19
+    , optparse-applicative >=0.14 && <0.19
+  default-language: Haskell2010
diff --git a/package.yaml b/package.yaml
new file mode 100644
--- /dev/null
+++ b/package.yaml
@@ -0,0 +1,63 @@
+name:                haskell-opentimestamps-client
+version:             0.4.1.0
+license:             BSD3
+#x-license:           BSD-3-Clause OR Apache-2.0
+author:              "A.H.M. (Mari) Donkers"
+maintainer:          "a.h.m.donkers@photonsphere.org"
+copyright:           "2025 A.H.M. (Mari) Donkers"
+git:                 "https://codeberg.org/photonsphere/haskell-opentimestamps-client"
+category:            Bitcoin, Finance, Network, Cryptography
+synopsis:            Haskell implementation of a basic OpenTimestamps client
+
+extra-source-files:
+- README.md
+- CHANGELOG.md
+- cabal.project
+- package.yaml
+- Makefile
+
+description:         Please see the README on Codeberg at <https://codeberg.org/photonsphere/haskell-opentimestamps-client#readme>
+
+ghc-options:
+- -Wall
+- -Wcompat
+- -Widentities
+- -Wincomplete-record-updates
+- -Wincomplete-uni-patterns
+- -Wmissing-export-lists
+- -Wmissing-home-modules
+- -Wpartial-fields
+- -Wredundant-constraints
+
+library:
+  source-dirs: src
+  dependencies:
+    - base >= 4.7 && < 5
+    - bytestring >= 0.10 && < 0.13
+    - cryptonite >= 0.25 && < 0.35
+    - directory >= 1.2 && < 1.4
+    - filepath >= 1.3 && < 1.5
+    - haskell-opentimestamps >= 0.5.5.0 && < 1.0
+    - memory >= 0.14 && < 0.19
+    - temporary >= 1.2 && < 1.4
+    - text >= 1.2 && < 2.2
+    - time >= 1.4 && < 1.13
+    - base16 >= 0.1 && < 2.0
+    - containers >= 0.5 && < 0.8
+
+executables:
+  haskell-opentimestamps-client-exe:
+    main:                Main.hs
+    source-dirs:         app
+    ghc-options:
+    - -threaded
+    - -rtsopts
+    - -with-rtsopts=-N
+    dependencies:
+    - base >= 4.7 && < 5
+    - bytestring >= 0.10 && < 0.13
+    - cryptonite >= 0.25 && < 0.35
+    - haskell-opentimestamps-client
+    - memory >= 0.14 && < 0.19
+    - optparse-applicative >= 0.14 && < 0.19
+
diff --git a/src/OpenTimestampsClient.hs b/src/OpenTimestampsClient.hs
new file mode 100644
--- /dev/null
+++ b/src/OpenTimestampsClient.hs
@@ -0,0 +1,200 @@
+{-# LANGUAGE GADTs #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module OpenTimestampsClient
+  ( info
+  , prune
+  , stamp
+  , upgrade
+  , verify
+  ) where
+
+import Control.Monad (unless, when)
+import Crypto.Hash (SHA256 (SHA256), hashWith)
+import qualified Data.ByteArray as BA
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy.Char8 as BSL
+import qualified Data.Text as T
+import qualified Data.Text.IO as TIO
+import Data.Time.Format (defaultTimeLocale, formatTime)
+import Data.Time.LocalTime (TimeZone (..), utcToZonedTime)
+import qualified OpenTimestamps as OT
+import qualified OpenTimestamps.DetachedTimestampFile as DTSF
+import OpenTimestamps.Timestamp (printHex, printTimestamp, timestampMsg)
+import qualified OpenTimestamps.Verify as Verify
+import System.Directory (doesFileExist, renameFile)
+import System.FilePath (addExtension, stripExtension, takeDirectory)
+import System.IO (hClose, hPutStrLn, stderr)
+import System.IO.Temp (withTempFile)
+
+data RPCCredentials where
+  RPCCredentials ::
+    {rpcUser :: T.Text, rpcPassword :: T.Text} ->
+    RPCCredentials
+  deriving (Show, Eq)
+
+readCookieFile :: FilePath -> IO (Maybe RPCCredentials)
+readCookieFile path = do
+  exists <- doesFileExist path
+  if exists
+    then do
+      content <- TIO.readFile path
+      case T.splitOn ":" content of
+        [user, pass] -> return $ Just $ RPCCredentials user pass
+        _ -> return Nothing
+    else do
+      hPutStrLn stderr $ "Cookie file not found at: " ++ path
+      return Nothing
+
+info :: FilePath -> IO ()
+info path =
+  processStream =<< BSL.readFile path
+  where
+    processStream otsFileContent = case DTSF.deserialize otsFileContent of
+      Left err ->
+        putStrLn $ "Error deserializing: " ++ show err
+      Right dtfs -> do
+        putStrLn $
+          "File sha256 hash: "
+            ++ printHex
+              ( timestampMsg
+                  (DTSF.timestamp dtfs)
+              )
+        putStrLn "Timestamp:"
+        putStrLn $ printTimestamp 0 (DTSF.timestamp dtfs)
+
+prune :: Bool -> FilePath -> [String] -> [String] -> IO ()
+prune verbose path verifyArgs discardArgs = do
+  when verbose $ putStrLn $ "Reading timestamp file: " ++ path
+  otsFileContent <- BSL.readFile path
+  when verbose $ putStrLn $ "Pruning timestamp: " ++ path
+  result <- OT.prune verifyArgs discardArgs otsFileContent
+  case result of
+    Left err -> do
+      hPutStrLn stderr $ "Error pruning timestamp: " ++ err
+    Right newOtsContent -> do
+      let dir = takeDirectory path
+      let tmpFileName = path ++ ".tmp"
+      tmpExists <- doesFileExist tmpFileName
+      when tmpExists $
+        fail $
+          "Intermediate temporary file " ++ tmpFileName ++ " already exists"
+      let bakFileName = path ++ ".bak"
+      bakExists <- doesFileExist bakFileName
+      when bakExists $
+        fail $
+          "Backup file " ++ bakFileName ++ " already exists"
+
+      withTempFile
+        dir
+        tmpFileName
+        ( \tempPath tempHandle -> do
+            BSL.hPutStr tempHandle newOtsContent
+            hClose tempHandle
+            renameFile path bakFileName
+            renameFile tempPath path
+            putStrLn $ "Timestamp pruned and saved to " ++ path
+        )
+
+stamp :: Bool -> [FilePath] -> [String] -> IO ()
+stamp verbose paths calendarUrls = do
+  mapM_ stampOne paths
+  where
+    stampOne path = do
+      let newOtsPath = addExtension path ".ots"
+      exists <- doesFileExist newOtsPath
+      when exists $
+        fail $
+          "Failed to create timestamp '" ++ newOtsPath ++ "' (file already exists)."
+      when verbose $ putStrLn $ "Reading content of: " ++ path
+      content <- BS.readFile path
+      -- TODO Use OpenTimestamps library function (t.b.d.) with type.
+      let digest = BA.convert (hashWith SHA256 content)
+      when verbose $ putStrLn $ "Stamping: " ++ path
+      sr <- OT.stamp calendarUrls digest
+      -- TODO Nicer error formatting.
+      let errors = OT.srErrors sr
+      if verbose
+        then mapM_ print errors
+        else
+          unless (null errors) $
+            hPutStrLn stderr "There were calender servers with errors (use global option -v for details)"
+      case OT.srTimestampFile sr of
+        Nothing -> hPutStrLn stderr "No timestamps could be retrieved"
+        Just dtsf -> do
+          let dtfsNew = OT.serialize dtsf
+          BSL.writeFile newOtsPath dtfsNew
+          putStrLn $ "Timestamp saved to " ++ newOtsPath
+
+upgrade :: Bool -> FilePath -> IO ()
+upgrade verbose path = do
+  when verbose $ putStrLn $ "Reading timestamp file: " ++ path
+  otsFileContent <- BSL.readFile path
+  when verbose $ putStrLn $ "Upgrading timestamp: " ++ path
+  -- TODO The `[]` is for calendarUrls (what to do with them?)
+
+  result <- OT.upgrade [] otsFileContent
+  case result of
+    Left err -> do
+      hPutStrLn stderr $ "Error upgrading timestamp: " ++ err
+    Right newOtsContent -> do
+      let dir = takeDirectory path
+      let tmpFileName = path ++ ".tmp"
+      tmpExists <- doesFileExist tmpFileName
+      when tmpExists $ fail $ "Intermediate temporary file " ++ tmpFileName ++ " already exists"
+      let bakFileName = path ++ ".bak"
+      bakExists <- doesFileExist bakFileName
+      when bakExists $ fail $ "Backup file " ++ bakFileName ++ " already exists"
+
+      withTempFile
+        dir
+        tmpFileName
+        ( \tempPath tempHandle -> do
+            BSL.hPutStr tempHandle newOtsContent
+            hClose tempHandle
+            renameFile path bakFileName
+            renameFile tempPath path
+            putStrLn $ "Timestamp upgraded and saved to " ++ path
+        )
+
+verify :: Bool -> FilePath -> IO ()
+verify verbose path = do
+  let cookieFilePath = "/home/mdo/.bitcoin/.cookie"
+  mrpcc <- readCookieFile cookieFilePath
+  let bitcoinConfig = case mrpcc of
+        Nothing ->
+          Verify.BitcoinConfig
+            { Verify.bitcoinHost = "localhost"
+            , Verify.bitcoinPort = 8332
+            , Verify.bitcoinUser = "bitcoin"
+            , Verify.bitcoinPass = "bitcoinpass"
+            }
+        Just rpcc ->
+          Verify.BitcoinConfig
+            { Verify.bitcoinHost = "localhost"
+            , Verify.bitcoinPort = 8332
+            , Verify.bitcoinUser = T.unpack $ rpcUser rpcc
+            , Verify.bitcoinPass = T.unpack $ rpcPassword rpcc
+            }
+
+  print $ "Cookiefile path: " ++ cookieFilePath
+  -- print $ "User: " ++ Verify.bitcoinUser bitcoinConfig
+  -- print $ "Password: " ++ Verify.bitcoinPass bitcoinConfig
+  case stripExtension ".ots" path of
+    Nothing -> hPutStrLn stderr $ "Not a timestamp? " ++ path
+    Just srcPath -> do
+      content <- BS.readFile srcPath
+      let digest = BA.convert (hashWith SHA256 content)
+      when verbose $ putStrLn $ "Reading timestamp file: " ++ path
+      otsFileContent <- BSL.readFile path
+      when verbose $ putStrLn $ "Verifying timestamp: " ++ path
+      -- TODO The `[]` is for calendarUrls (what to do with them?)
+
+      result <- OT.verify bitcoinConfig [] otsFileContent digest
+      case result of
+        Right (blockHeight, utcTime) -> do
+          let zonedTime = utcToZonedTime (TimeZone (-(1 * 60 * 4)) False "CEST") utcTime -- TODO Assuming CEST is UTC+2, so -2 hours from UTC
+          let formattedTime = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S %Z" zonedTime
+          putStrLn $
+            "Success! Bitcoin block " ++ show blockHeight ++ " attests existence as of " ++ formattedTime
+        Left err -> putStrLn $ "Failed " ++ show err
