diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,19 @@
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 
+## [2.0.1] - 2026-11-07
+
+### Changed
+
+- Documentation: the README explains the purpose and design of the bindings,
+  with installation steps for every platform, a tutorial, a usage guide, the
+  differences from Bitcoin Core and security notes. Every module, including
+  the foreign imports, has Haddock, and the main module and key functions say
+  why they work the way they do.
+- CI builds, tests and documents the package with Stack, against the
+  libsecp256k1 of Ubuntu and with `stack --nix`.
+- Build with Stackage nightly-2026-11-07.
+
 ## [2.0.0] - 2026-11-03
 
 This release follows a review of the bindings, including the changes of
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,453 @@
+# secp256k1-haskell
+
+[![Hackage](https://img.shields.io/hackage/v/secp256k1-haskell.svg)](https://hackage.haskell.org/package/secp256k1-haskell)
+[![CI](https://github.com/jprupp/secp256k1-haskell/actions/workflows/ci.yml/badge.svg)](https://github.com/jprupp/secp256k1-haskell/actions/workflows/ci.yml)
+[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jprupp/secp256k1-haskell/blob/master/LICENSE)
+
+Haskell bindings to [libsecp256k1](https://github.com/bitcoin-core/secp256k1),
+the library that Bitcoin Core uses for elliptic curve cryptography on the
+secp256k1 curve. It covers ECDSA signatures, BIP-340 Schnorr signatures,
+BIP-341 Taproot tweaks, key tweaking for BIP-32 derivation, and ECDSA public
+key recovery for Bitcoin signed messages.
+
+- [API documentation](https://hackage.haskell.org/package/secp256k1-haskell)
+- [Changelog](https://github.com/jprupp/secp256k1-haskell/blob/master/CHANGELOG.md)
+- [Report a bug](https://github.com/jprupp/secp256k1-haskell/issues)
+
+## Contents
+
+- [Why secp256k1-haskell](#why-secp256k1-haskell)
+- [Design principles](#design-principles)
+- [Features](#features)
+- [Installation](#installation)
+- [Quick start](#quick-start)
+- [Tutorial: sign, verify and serialize](#tutorial-sign-verify-and-serialize)
+- [Concepts](#concepts)
+- [Usage guide](#usage-guide)
+- [Differences from Bitcoin Core](#differences-from-bitcoin-core)
+- [Module overview](#module-overview)
+- [Security notes](#security-notes)
+- [Development](#development)
+- [Related projects](#related-projects)
+- [License](#license)
+
+## Why secp256k1-haskell
+
+Elliptic curve cryptography is easy to write and very hard to get right. A
+signing routine whose running time depends on the secret key leaks it through
+timing. A nonce that repeats once reveals the key outright. An edge case in
+point arithmetic accepts a forged signature. These are not theoretical: each has
+cost real users real money.
+
+libsecp256k1 is the answer the Bitcoin community converged on. It is
+constant-time wherever secrets are involved, blinds secret key operations
+against side channels, is tested exhaustively, and has been reviewed for over a
+decade. It is what Bitcoin Core runs.
+
+This package lets a Haskell program use that library instead of reimplementing
+it, and wraps it in types that make the usual mistakes hard to make. It is the
+cryptographic foundation of [haskoin-core](https://github.com/jprupp/haskoin-core)
+and, through it, of the rest of the Haskoin libraries.
+
+## Design principles
+
+- **Valid by construction.** Every key, signature, hash and tweak goes through
+  a smart constructor or an import function that checks it. A `PubKey` is
+  always a point on the curve; a `Sig` always has both components in range.
+  Invalid input is refused where it enters the program, not deep inside a
+  computation.
+- **Never abort the process.** By default libsecp256k1 calls `abort()` on an
+  illegal argument, which kills the whole Haskell program and cannot be caught.
+  Every context this package creates installs a callback that makes the C
+  function fail instead, so a bad argument becomes `Nothing`, `False` or an
+  exception.
+- **Pure where the mathematics is pure.** Signing with deterministic nonces,
+  verifying, deriving and tweaking keys are pure functions. Only operations
+  that need fresh randomness run in `IO`.
+- **Standard encodings at the edges.** `Show`, `Read` and `IsString` use SEC1,
+  compact or plain hex, never libsecp256k1's internal memory layout, which
+  differs between platforms and library versions.
+- **Explicit about Bitcoin Core.** Where Bitcoin Core goes beyond the
+  standards, such as grinding signatures to a low R value or accepting high-S
+  signatures from old blocks, this package offers the behaviour as a separate,
+  documented function instead of silently picking one.
+- **Thin.** Hashing, address encoding, BIP-32 paths and transaction formats
+  belong in higher-level libraries such as haskoin-core.
+
+## Features
+
+| Area                 | What you get                                                                                   |
+| -------------------- | ---------------------------------------------------------------------------------------------- |
+| ECDSA                | Deterministic RFC 6979 signing, Bitcoin Core's low-R grinding, verification, lower-S normalization |
+| Encodings            | SEC1 public keys (33 or 65 bytes), DER and compact signatures, hex `Show`/`Read`/`IsString`    |
+| Schnorr (BIP-340)    | Signing (deterministic, with given or fresh auxiliary randomness), verification, x-only keys    |
+| Taproot (BIP-341)    | Tweaking an x-only public key and its secret key                                               |
+| Key arithmetic       | Add or multiply secret and public keys by a tweak, negate, combine public keys                 |
+| Recovery             | Recoverable ECDSA signatures and public key recovery, as Bitcoin signed messages use           |
+| Safety               | Validated values, no aborts, length checks before every C call, contexts shared across threads |
+| Testing              | QuickCheck `Arbitrary` instances for secret keys, hashes and tweaks                            |
+
+### What is not included
+
+- No hashing. Pass a 32-byte hash you computed with a hash library, such as
+  `cryptohash-sha256` or haskoin-core.
+- No ECDH, MuSig2 or adaptor signatures, although libsecp256k1 or its
+  forks offer some of them.
+- No secure erasure of secret keys from memory. See [Security notes](#security-notes).
+
+## Installation
+
+### System library
+
+The package needs [libsecp256k1](https://github.com/bitcoin-core/secp256k1)
+**0.2.0 or later** and `pkg-config` to find it. Public key recovery also needs
+the library's recovery module, which the packages below include.
+
+```sh
+# Debian and Ubuntu (24.04 ships 0.2.0)
+sudo apt-get install libsecp256k1-dev pkg-config
+
+# Fedora
+sudo dnf install libsecp256k1-devel pkgconf
+
+# macOS with Homebrew
+brew install secp256k1 pkg-config
+
+# Nix
+nix-shell -p secp256k1 pkg-config
+```
+
+Check what you have:
+
+```sh
+pkg-config --modversion libsecp256k1   # must print 0.2.0 or later
+```
+
+Older distributions, such as Debian 11, ship an earlier version. Build the
+library from source then:
+
+```sh
+git clone https://github.com/bitcoin-core/secp256k1.git
+cd secp256k1
+./autogen.sh
+./configure --enable-module-recovery
+make && sudo make install
+```
+
+If your library was built without the recovery module, build this package with
+the `recovery` flag off: `stack build --flag secp256k1-haskell:-recovery`, or
+`cabal build -f -recovery`. Everything except `Crypto.Secp256k1.Recovery` still
+works.
+
+### Step by step with Stack
+
+1. Install [Stack](https://docs.haskellstack.org/) and the library above.
+2. Create a project:
+
+   ```sh
+   stack new sig-demo simple
+   cd sig-demo
+   ```
+
+3. Add `secp256k1-haskell` and `bytestring` to `build-depends` in
+   `sig-demo.cabal`.
+4. If your resolver does not include the package, add it to `stack.yaml`:
+
+   ```yaml
+   extra-deps:
+     - secp256k1-haskell-2.0.1
+     - base16-2.0
+   ```
+
+5. Put the [quick start](#quick-start) program in `src/Main.hs` and run it with
+   `stack run`. It prints `True` three times.
+
+### With Cabal
+
+```cabal
+build-depends:
+    base
+  , bytestring
+  , secp256k1-haskell
+```
+
+Run `cabal update` first, then `cabal build`.
+
+## Quick start
+
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+
+import Crypto.Secp256k1
+
+main :: IO ()
+main = do
+  ctx <- createContext
+
+  -- Hex literals go through IsString, which validates them.
+  let key = "c7e5a2b4f4c5d7e8a9b0c1d2e3f405162738495a6b7c8d9e0f1a2b3c4d5e6f70" :: SecKey
+      hash = "4b688df40bcedbe641ddb16ff0a1842d9c67ea1c3bf63f3e0471baa664531d1a" :: Msg
+      pub = derivePubKey ctx key
+
+  -- ECDSA
+  let s = signMsg ctx key hash
+  print (verifySig ctx pub s hash)
+
+  -- BIP-340 Schnorr, with fresh auxiliary randomness
+  schnorr <- signSchnorrIO ctx key hash
+  print (verifySchnorr ctx pub schnorr hash)
+
+  -- Round trip through the standard encodings
+  let Just pub' = importPubKey ctx (exportPubKey ctx True pub)
+  print (pub == pub')
+```
+
+## Tutorial: sign, verify and serialize
+
+This walks through what a wallet or a protocol implementation typically does,
+and why each step looks the way it does.
+
+### 1. Create one context and share it
+
+```haskell
+ctx <- createContext
+```
+
+A context holds precomputed tables and random blinding values. Creating one is
+relatively expensive, and one context can be used by every thread for the life
+of the program. `createContext` randomizes it once, which is what libsecp256k1
+recommends.
+
+### 2. Get keys from bytes
+
+```haskell
+case secKey keyBytes of
+  Nothing -> fail "not a valid secret key"
+  Just key -> ...
+```
+
+`secKey` refuses 0 and numbers not below the curve order, so a `SecKey` is
+always usable. In practice, keys come from a wallet: from a BIP-39 mnemonic and
+BIP-32 derivation in haskoin-core, or from 32 bytes of the operating system's
+secure randomness (`getEntropy 32` from the `entropy` package).
+
+### 3. Hash, then sign
+
+The library signs 32-byte hashes, not messages. Which hash to use is part of
+the protocol: Bitcoin transactions use double SHA-256 of a specific
+serialization, BIP-340 uses tagged hashes. Build the `Msg` from your hash:
+
+```haskell
+let Just m = msg hashBytes
+    s = signMsgLowR ctx key m -- or signMsg for plain RFC 6979
+```
+
+Signing is deterministic: the same key and hash give the same signature, so
+there is no random number generator to get wrong.
+
+### 4. Serialize for the wire
+
+Never send the `get` bytes of a `PubKey` or `Sig`: they are libsecp256k1's
+internal layout. Use the standard encodings:
+
+```haskell
+let pubBytes = exportPubKey ctx True pub -- 33-byte compressed SEC1
+    derBytes = exportSig ctx s -- DER, as in Bitcoin transactions
+    compactBytes = exportCompactSig ctx s -- 64 bytes r || s
+```
+
+### 5. Parse and verify what you receive
+
+```haskell
+verified = fromMaybe False $ do
+  pub <- importPubKey ctx pubBytes
+  s <- importSig ctx derBytes
+  m <- msg hashBytes
+  return (verifySig ctx pub (normalizeSig ctx s) m)
+```
+
+Every import returns `Maybe`, so malformed input is handled in one place.
+`normalizeSig` is there because `verifySig` only accepts lower-S signatures;
+apply it when the signature may come from software that does not produce them,
+such as old Bitcoin transactions. Leave it out when your protocol requires
+lower-S, as Bitcoin's relay rules do.
+
+### 6. Move to Schnorr where the protocol allows
+
+```haskell
+sig <- signSchnorrIO ctx key m
+let ok = verifySchnorr ctx pub sig m
+    xonly = exportXOnlyPubKey ctx pub -- 32 bytes, as BIP-340 publishes keys
+```
+
+Schnorr signatures are always 64 bytes, are not malleable, and are what Taproot
+uses. `signSchnorrIO` mixes fresh randomness into the nonce, as BIP-340
+recommends, to protect against fault attacks.
+
+## Concepts
+
+### Values
+
+| Type         | What it holds                                            | Built with                              |
+| ------------ | -------------------------------------------------------- | --------------------------------------- |
+| `SecKey`     | 32-byte number from 1 to n − 1                           | `secKey`, `IsString`                    |
+| `PubKey`     | A point on the curve                                     | `importPubKey`, `derivePubKey`          |
+| `Msg`        | 32-byte hash                                             | `msg`                                   |
+| `Sig`        | ECDSA signature with both components from 1 to n − 1    | `signMsg`, `importSig`, `importCompactSig` |
+| `CompactSig` | 64 bytes, r ‖ s                                          | `exportCompactSig`, `compactSig`        |
+| `SchnorrSig` | 64-byte BIP-340 signature                                | `signSchnorr`, `schnorrSig`             |
+| `Tweak`      | 32-byte number from 0 to n − 1                           | `tweak`                                 |
+| `RecSig`     | ECDSA signature with its recovery id                     | `signRecMsg`, `importCompactRecSig`     |
+
+Here n is the order of the curve. The raw constructors are hidden, so these
+are the only ways to build values. Each type has a `get` field with its bytes.
+
+### Contexts
+
+Every operation takes a `Ctx`. It is created once, shared freely between
+threads, and destroyed by the garbage collector. The only operation that
+modifies a context is `randomizeContext`, which needs exclusive access; most
+programs never call it.
+
+### Errors
+
+Parsing and constructing return `Maybe`. Arithmetic that can produce an
+invalid result, such as a tweak that cancels a key, returns `Maybe`. Pure
+operations on valid values, such as signing, cannot fail and return their
+result directly. Programming errors, such as a value of the wrong length built
+with an internal constructor, raise an exception.
+
+## Usage guide
+
+### Lower-S and low-R
+
+```haskell
+isLowS ctx s -- the form verifySig accepts
+isLowR ctx s -- r below 2^255, so DER is at most 70 bytes
+normalizeSig ctx s -- the lower-S form of any signature
+```
+
+### Key tweaking
+
+```haskell
+let Just t = tweak tweakBytes
+    Just childKey = tweakAddSecKey ctx parentKey t
+    Just childPub = tweakAddPubKey ctx parentPub t
+-- derivePubKey ctx childKey == childPub
+```
+
+This is the operation behind BIP-32 derivation; the tweak itself comes from an
+HMAC of the chain code, computed by haskoin-core. `tweakMulSecKey`,
+`tweakMulPubKey`, `tweakNegate` and `combinePubKeys` cover the other group
+operations.
+
+### Taproot
+
+```haskell
+let Just t = tweak taprootTweakBytes -- tagged hash of key and script tree
+    Just outputKey = tweakAddXOnlyPubKey ctx internalKey t
+    Just outputSecKey = tweakAddXOnlySecKey ctx internalSecKey t
+```
+
+Computing the tweak, a BIP-341 tagged hash, is left to the caller.
+
+### Bitcoin signed messages
+
+```haskell
+import Crypto.Secp256k1.Recovery
+
+let r = signRecMsg ctx key m
+    (compact, recId) = exportCompactRecSig ctx r
+-- the 65-byte message signature is a header byte (27 + recId, + 4 if the key
+-- is compressed) followed by the 64 compact bytes
+recover ctx r m -- Just the signer's PubKey
+```
+
+### Testing with QuickCheck
+
+`SecKey`, `Msg` and `Tweak` have `Arbitrary` instances, so properties can
+range over them directly. With a context `ctx` created once for the test suite:
+
+```haskell
+prop_roundTrip :: SecKey -> Msg -> Bool
+prop_roundTrip key m =
+  verifySig ctx (derivePubKey ctx key) (signMsg ctx key m) m
+```
+
+## Differences from Bitcoin Core
+
+| Topic                      | This package                                     | Bitcoin Core                          |
+| -------------------------- | ------------------------------------------------ | ------------------------------------- |
+| ECDSA nonce                | `signMsg`: plain RFC 6979                        | Grinds for low R                      |
+| Low-R signatures           | `signMsgLowR`: same grinding, same signatures    | Default since 0.17                    |
+| High-S signatures          | `verifySig` refuses them; use `normalizeSig`     | Normalizes before verifying           |
+| Verify after ECDSA signing | No                                               | Yes, as a precaution                  |
+| Taproot tweak when signing | Separate step: `tweakAddXOnlySecKey`             | Built into Schnorr signing            |
+| Message signatures         | `signRecMsg`, no grinding                        | Same, so signatures are identical     |
+
+## Module overview
+
+| Module                                  | Contents                                                  |
+| --------------------------------------- | --------------------------------------------------------- |
+| `Crypto.Secp256k1`                      | The public API: contexts, keys, ECDSA, Schnorr, tweaks    |
+| `Crypto.Secp256k1.Recovery`             | Recoverable signatures and public key recovery            |
+| `Crypto.Secp256k1.Internal.*`           | Raw constructors and foreign imports; no stability promise |
+
+## Security notes
+
+- **Secret keys stay in memory.** A `SecKey` is an ordinary `ByteString`. The
+  garbage collector may copy it, and nothing clears it when it is no longer
+  used. If your threat model includes an attacker reading process memory,
+  keep keys in a hardware device instead.
+- **`Show` prints secret keys.** Showing a record that holds a `SecKey`, in a
+  log line or an error message, reveals it.
+- **Verification only proves what was signed.** It is your protocol's job to
+  hash the right data; signing a hash an attacker chose is as good as signing
+  whatever they want.
+- **Use fresh randomness for Schnorr when you can.** `signSchnorrIO` protects
+  against fault attacks that deterministic signing does not.
+- **Keep libsecp256k1 up to date.** Security fixes land in the C library;
+  this package only binds it.
+
+## Development
+
+```sh
+git clone https://github.com/jprupp/secp256k1-haskell.git
+cd secp256k1-haskell
+
+stack build                        # build against the system library
+stack --nix build                  # or take GHC and libsecp256k1 from nixpkgs
+stack test                         # run the tests
+stack haddock --no-haddock-deps    # build the API documentation
+```
+
+`package.yaml` is the source of the package description. Change it rather than
+`secp256k1-haskell.cabal`, which [hpack](https://github.com/sol/hpack)
+generates.
+
+The tests are in `test/` and use [hspec](https://hspec.github.io/) and
+QuickCheck, with published test vectors from RFC 6979, Bitcoin Core and
+BIP-340. CI runs them with the system library and
+with `stack --nix`.
+
+### Contributing
+
+Pull requests are welcome.
+
+- Add tests for the behaviour you change, with published test vectors where
+  they exist.
+- Keep the documentation in step with the code, as `CLAUDE.md` describes.
+- Describe the change in `CHANGELOG.md`, which follows
+  [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
+
+## Related projects
+
+- [haskoin-core](https://github.com/jprupp/haskoin-core) builds Bitcoin keys,
+  addresses, transactions and PSBTs on this package.
+- [libsecp256k1](https://github.com/bitcoin-core/secp256k1) is the C library
+  underneath.
+
+## License
+
+secp256k1-haskell is released under the
+[MIT License](https://github.com/jprupp/secp256k1-haskell/blob/master/LICENSE).
diff --git a/secp256k1-haskell.cabal b/secp256k1-haskell.cabal
--- a/secp256k1-haskell.cabal
+++ b/secp256k1-haskell.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           secp256k1-haskell
-version:        2.0.0
+version:        2.0.1
 synopsis:       Bindings for secp256k1
 description:    Sign and verify signatures using the secp256k1 library.
 category:       Crypto
@@ -18,6 +18,7 @@
 build-type:     Simple
 extra-source-files:
     CHANGELOG.md
+    README.md
 
 source-repository head
   type: git
diff --git a/src/Crypto/Secp256k1.hs b/src/Crypto/Secp256k1.hs
--- a/src/Crypto/Secp256k1.hs
+++ b/src/Crypto/Secp256k1.hs
@@ -3,6 +3,41 @@
 -- | ECDSA and BIP-340 Schnorr signatures on the secp256k1 curve, through
 -- <https://github.com/bitcoin-core/secp256k1 libsecp256k1>.
 --
+-- == Purpose
+--
+-- secp256k1 is the elliptic curve of Bitcoin, and libsecp256k1 is the C
+-- library Bitcoin Core uses for it: constant-time where secrets are involved,
+-- hardened against side channels, exhaustively tested and reviewed. Getting
+-- elliptic curve cryptography right is hard enough that a Haskell program
+-- should not reimplement it. This package gives Haskell the same library, with
+-- types that make the common mistakes hard to make.
+--
+-- == Design
+--
+-- * __Valid by construction.__ Every value goes through a smart constructor or
+--   an import function that checks it. Code that holds a 'PubKey' or a 'Sig'
+--   never has to wonder whether it is a point on the curve or a well-formed
+--   signature, and invalid input is refused at the boundary where it arrives.
+--
+-- * __Never abort.__ The C library calls @abort()@ on an illegal argument by
+--   default, which kills the whole Haskell process and cannot be caught. Every
+--   context this package creates replaces that callback, so a bad argument
+--   becomes 'Nothing', 'False' or an exception instead.
+--
+-- * __Pure where the mathematics is pure.__ Signing, verifying and tweaking are
+--   deterministic functions of their inputs, so they are pure functions here.
+--   Only the operations that need fresh randomness, such as 'signSchnorrIO'
+--   and 'randomizeContext', run in 'IO'.
+--
+-- * __Standard encodings at the edges.__ Values print and parse in the encodings
+--   other software understands (SEC1, DER, compact, BIP-340), never in the
+--   library's internal memory layout, which differs between platforms.
+--
+-- * __Bitcoin Core compatibility is explicit.__ Where Bitcoin Core does
+--   something beyond the standards, such as low-R grinding or accepting high-S
+--   signatures from old blocks, this package offers it as a separate function
+--   and documents the difference, rather than silently picking one behaviour.
+--
 -- == Values
 --
 -- Every value is built through a smart constructor or an import function
diff --git a/src/Crypto/Secp256k1/Internal/Base.hs b/src/Crypto/Secp256k1/Internal/Base.hs
--- a/src/Crypto/Secp256k1/Internal/Base.hs
+++ b/src/Crypto/Secp256k1/Internal/Base.hs
@@ -274,6 +274,7 @@
       0xbf, 0xd2, 0x5e, 0x8c, 0xd0, 0x36, 0x41, 0x41
     ]
 
+-- | Thirty-two zero bytes.
 zero32 :: ByteString
 zero32 = BS.replicate 32 0
 
@@ -502,6 +503,12 @@
 -- | Verify a signature over a message hash. 'True' means the signature is
 -- valid for the key and the hash and is in lower-S form. A valid signature
 -- in high-S form gives 'False'. Apply 'normalizeSig' first to accept it.
+--
+-- The lower-S rule exists because ECDSA signatures are malleable: if (r, s)
+-- is valid, so is (r, n − s). Anyone could change a transaction's signature,
+-- and therefore its id, without the key. Accepting only one of the two forms
+-- removes that freedom, which is why libsecp256k1 and Bitcoin's relay rules
+-- insist on it.
 verifySig :: Ctx -> PubKey -> Sig -> Msg -> Bool
 verifySig (Ctx fctx) (PubKey pub_key) (Sig sig_bs) (Msg m) =
   unsafePerformIO $ withForeignPtr fctx $ \ctx ->
@@ -512,6 +519,12 @@
 
 -- | Sign a message hash. The nonce comes from RFC 6979, so the same key and
 -- hash always give the same signature. The signature is in lower-S form.
+--
+-- Deterministic nonces are the reason this is a pure function, and they are a
+-- safety feature, not a convenience: an ECDSA nonce that repeats, or that is
+-- even slightly predictable, reveals the secret key. Deriving it from the key
+-- and the hash, as RFC 6979 does, removes the need for a good random number
+-- generator at signing time.
 signMsg :: Ctx -> SecKey -> Msg -> Sig
 signMsg ctx key m = signMsgWith ctx key m Nothing
 
@@ -542,6 +555,10 @@
 -- nonce data (a 32-bit little-endian counter padded to 32 bytes), so the
 -- signatures are the same as Bitcoin Core's. On average it takes two
 -- attempts. The result is still deterministic.
+--
+-- Use it where the size of a signature matters, as in Bitcoin transactions,
+-- where every byte costs a fee, or where signatures must match Bitcoin Core's
+-- byte for byte. Use 'signMsg' where the RFC 6979 test vectors must hold.
 signMsgLowR :: Ctx -> SecKey -> Msg -> Sig
 signMsgLowR ctx key m = go 0 (signMsg ctx key m)
   where
@@ -556,6 +573,9 @@
         <> BS.replicate 28 0
 
 -- | Compute the public key of a secret key.
+--
+-- It is a pure function and cannot fail, because a 'SecKey' is valid by
+-- construction.
 derivePubKey :: Ctx -> SecKey -> PubKey
 derivePubKey (Ctx fctx) (SecKey sec_key) =
   unsafePerformIO $ withForeignPtr fctx $ \ctx ->
@@ -567,6 +587,11 @@
 -- | Add a tweak to a secret key: @key + tweak mod n@. 'Nothing' when the
 -- result would be zero, which happens when the tweak is the negation of the
 -- key.
+--
+-- Tweaks are the building block of key derivation: BIP-32 child keys are a
+-- parent key plus a tweak, and Taproot outputs are an internal key plus a
+-- tweak. 'tweakAddPubKey' does the same on the public side, which is what lets
+-- a watch-only wallet derive addresses without any secret.
 tweakAddSecKey :: Ctx -> SecKey -> Tweak -> Maybe SecKey
 tweakAddSecKey (Ctx fctx) (SecKey sec_key) (Tweak t) =
   unsafePerformIO $ withForeignPtr fctx $ \ctx ->
diff --git a/src/Crypto/Secp256k1/Internal/BaseOps.hs b/src/Crypto/Secp256k1/Internal/BaseOps.hs
--- a/src/Crypto/Secp256k1/Internal/BaseOps.hs
+++ b/src/Crypto/Secp256k1/Internal/BaseOps.hs
@@ -1,3 +1,12 @@
+-- | Foreign imports of the core, @extrakeys@ and @schnorrsig@ modules of libsecp256k1.
+--
+-- Each function is a direct binding to the C function of the same name in
+-- @secp256k1.h@, @secp256k1_extrakeys.h@ or @secp256k1_schnorrsig.h@. The
+-- bindings are @safe@ calls: signing and verifying take tens of microseconds,
+-- and a @safe@ call lets other Haskell threads run meanwhile. Never call these
+-- directly with unchecked inputs; "Crypto.Secp256k1.Internal.Base" validates
+-- lengths and contents first. This module is internal and has no API
+-- stability guarantee.
 module Crypto.Secp256k1.Internal.BaseOps where
 
 import Crypto.Secp256k1.Internal.ForeignTypes
@@ -24,6 +33,7 @@
     CUInt (..),
   )
 
+-- | Binding to @secp256k1_ec_pubkey_parse@.
 foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_parse"
   ecPubKeyParse ::
     Ptr LCtx ->
@@ -34,6 +44,7 @@
     CSize ->
     IO Ret
 
+-- | Binding to @secp256k1_ec_pubkey_serialize@.
 foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_serialize"
   ecPubKeySerialize ::
     Ptr LCtx ->
@@ -45,6 +56,7 @@
     SerFlags ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_signature_parse_compact@.
 foreign import ccall safe "secp256k1.h secp256k1_ecdsa_signature_parse_compact"
   ecdsaSignatureParseCompact ::
     Ptr LCtx ->
@@ -52,6 +64,7 @@
     Ptr Compact64 ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_signature_parse_der@.
 foreign import ccall safe "secp256k1.h secp256k1_ecdsa_signature_parse_der"
   ecdsaSignatureParseDer ::
     Ptr LCtx ->
@@ -62,6 +75,7 @@
     CSize ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_signature_serialize_der@.
 foreign import ccall safe "secp256k1.h secp256k1_ecdsa_signature_serialize_der"
   ecdsaSignatureSerializeDer ::
     Ptr LCtx ->
@@ -72,6 +86,7 @@
     Ptr Sig64 ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_signature_serialize_compact@.
 foreign import ccall safe "secp256k1.h secp256k1_ecdsa_signature_serialize_compact"
   ecdsaSignatureSerializeCompact ::
     Ptr LCtx ->
@@ -79,6 +94,7 @@
     Ptr Sig64 ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_verify@.
 foreign import ccall safe "secp256k1.h secp256k1_ecdsa_verify"
   ecdsaVerify ::
     Ptr LCtx ->
@@ -87,6 +103,7 @@
     Ptr PubKey64 ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_signature_normalize@.
 foreign import ccall safe "secp256k1.h secp256k1_ecdsa_signature_normalize"
   ecdsaSignatureNormalize ::
     Ptr LCtx ->
@@ -96,6 +113,7 @@
     Ptr Sig64 ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_sign@.
 foreign import ccall safe "secp256k1.h secp256k1_ecdsa_sign"
   ecdsaSign ::
     Ptr LCtx ->
@@ -107,12 +125,14 @@
     Ptr a ->
     IO Ret
 
+-- | Binding to @secp256k1_ec_seckey_verify@.
 foreign import ccall safe "secp256k1.h secp256k1_ec_seckey_verify"
   ecSecKeyVerify ::
     Ptr LCtx ->
     Ptr SecKey32 ->
     IO Ret
 
+-- | Binding to @secp256k1_ec_pubkey_create@.
 foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_create"
   ecPubKeyCreate ::
     Ptr LCtx ->
@@ -120,6 +140,7 @@
     Ptr SecKey32 ->
     IO Ret
 
+-- | Binding to @secp256k1_ec_seckey_tweak_add@.
 foreign import ccall safe "secp256k1.h secp256k1_ec_seckey_tweak_add"
   ecSecKeyTweakAdd ::
     Ptr LCtx ->
@@ -127,12 +148,14 @@
     Ptr Tweak32 ->
     IO Ret
 
+-- | Binding to @secp256k1_ec_seckey_negate@.
 foreign import ccall safe "secp256k1.h secp256k1_ec_seckey_negate"
   ecTweakNegate ::
     Ptr LCtx ->
     Ptr Tweak32 ->
     IO Ret
 
+-- | Binding to @secp256k1_ec_pubkey_tweak_add@.
 foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_tweak_add"
   ecPubKeyTweakAdd ::
     Ptr LCtx ->
@@ -140,6 +163,7 @@
     Ptr Tweak32 ->
     IO Ret
 
+-- | Binding to @secp256k1_ec_seckey_tweak_mul@.
 foreign import ccall safe "secp256k1.h secp256k1_ec_seckey_tweak_mul"
   ecSecKeyTweakMul ::
     Ptr LCtx ->
@@ -147,6 +171,7 @@
     Ptr Tweak32 ->
     IO Ret
 
+-- | Binding to @secp256k1_ec_pubkey_tweak_mul@.
 foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_tweak_mul"
   ecPubKeyTweakMul ::
     Ptr LCtx ->
@@ -154,6 +179,7 @@
     Ptr Tweak32 ->
     IO Ret
 
+-- | Binding to @secp256k1_ec_pubkey_combine@.
 foreign import ccall safe "secp256k1.h secp256k1_ec_pubkey_combine"
   ecPubKeyCombine ::
     Ptr LCtx ->
@@ -168,6 +194,7 @@
 -- The following functions belong to the @extrakeys@ and @schnorrsig@ modules
 -- of libsecp256k1, enabled by default since version 0.2.0.
 
+-- | Binding to @secp256k1_xonly_pubkey_from_pubkey@.
 foreign import ccall safe "secp256k1_extrakeys.h secp256k1_xonly_pubkey_from_pubkey"
   xOnlyPubKeyFromPubKey ::
     Ptr LCtx ->
@@ -177,6 +204,7 @@
     Ptr PubKey64 ->
     IO Ret
 
+-- | Binding to @secp256k1_xonly_pubkey_serialize@.
 foreign import ccall safe "secp256k1_extrakeys.h secp256k1_xonly_pubkey_serialize"
   xOnlyPubKeySerialize ::
     Ptr LCtx ->
@@ -185,6 +213,7 @@
     Ptr XOnlyPubKey64 ->
     IO Ret
 
+-- | Binding to @secp256k1_xonly_pubkey_tweak_add@.
 foreign import ccall safe "secp256k1_extrakeys.h secp256k1_xonly_pubkey_tweak_add"
   xOnlyPubKeyTweakAdd ::
     Ptr LCtx ->
@@ -194,6 +223,7 @@
     Ptr Tweak32 ->
     IO Ret
 
+-- | Binding to @secp256k1_keypair_create@.
 foreign import ccall safe "secp256k1_extrakeys.h secp256k1_keypair_create"
   keyPairCreate ::
     Ptr LCtx ->
@@ -201,6 +231,7 @@
     Ptr SecKey32 ->
     IO Ret
 
+-- | Binding to @secp256k1_keypair_sec@.
 foreign import ccall safe "secp256k1_extrakeys.h secp256k1_keypair_sec"
   keyPairSec ::
     Ptr LCtx ->
@@ -208,6 +239,7 @@
     Ptr KeyPair96 ->
     IO Ret
 
+-- | Binding to @secp256k1_keypair_xonly_pub@.
 foreign import ccall safe "secp256k1_extrakeys.h secp256k1_keypair_xonly_pub"
   keyPairXOnlyPub ::
     Ptr LCtx ->
@@ -217,6 +249,7 @@
     Ptr KeyPair96 ->
     IO Ret
 
+-- | Binding to @secp256k1_keypair_xonly_tweak_add@.
 foreign import ccall safe "secp256k1_extrakeys.h secp256k1_keypair_xonly_tweak_add"
   keyPairXOnlyTweakAdd ::
     Ptr LCtx ->
@@ -224,6 +257,7 @@
     Ptr Tweak32 ->
     IO Ret
 
+-- | Binding to @secp256k1_schnorrsig_sign32@.
 foreign import ccall safe "secp256k1_schnorrsig.h secp256k1_schnorrsig_sign32"
   schnorrSigSign32 ::
     Ptr LCtx ->
@@ -234,6 +268,7 @@
     Ptr AuxRand32 ->
     IO Ret
 
+-- | Binding to @secp256k1_schnorrsig_verify@.
 foreign import ccall safe "secp256k1_schnorrsig.h secp256k1_schnorrsig_verify"
   schnorrSigVerify ::
     Ptr LCtx ->
diff --git a/src/Crypto/Secp256k1/Internal/Context.hs b/src/Crypto/Secp256k1/Internal/Context.hs
--- a/src/Crypto/Secp256k1/Internal/Context.hs
+++ b/src/Crypto/Secp256k1/Internal/Context.hs
@@ -108,25 +108,31 @@
 signVerify :: CtxFlags
 signVerify = 0x0301
 
+-- | Binding to @secp256k1_context_create@.
 foreign import ccall safe "secp256k1.h secp256k1_context_create"
   contextCreate ::
     CtxFlags ->
     IO (Ptr LCtx)
 
+-- | Binding to @secp256k1_context_clone@.
 foreign import ccall safe "secp256k1.h secp256k1_context_clone"
   contextClone ::
     Ptr LCtx ->
     IO (Ptr LCtx)
 
+-- | Binding to @secp256k1_context_destroy@.
 foreign import ccall safe "secp256k1.h secp256k1_context_destroy"
   contextDestroy :: Ptr LCtx -> IO ()
 
+-- | Pointer to @secp256k1_context_destroy@, for use as a finalizer or callback.
 foreign import ccall safe "secp256k1.h &secp256k1_context_destroy"
   contextDestroyFunPtr :: FunPtr (Ptr LCtx -> IO ())
 
+-- | Pointer to @secp256k1_haskell_illegal_callback@, for use as a finalizer or callback.
 foreign import ccall safe "&secp256k1_haskell_illegal_callback"
   illegalCallbackFunPtr :: FunPtr (CString -> Ptr a -> IO ())
 
+-- | Binding to @secp256k1_context_set_illegal_callback@.
 foreign import ccall safe "secp256k1.h secp256k1_context_set_illegal_callback"
   setIllegalCallback ::
     Ptr LCtx ->
@@ -136,6 +142,7 @@
     Ptr a ->
     IO ()
 
+-- | Binding to @secp256k1_context_set_error_callback@.
 foreign import ccall safe "secp256k1.h secp256k1_context_set_error_callback"
   setErrorCallback ::
     Ptr LCtx ->
@@ -145,6 +152,7 @@
     Ptr a ->
     IO ()
 
+-- | Binding to @secp256k1_context_randomize@.
 foreign import ccall safe "secp256k1.h secp256k1_context_randomize"
   contextRandomize ::
     Ptr LCtx ->
diff --git a/src/Crypto/Secp256k1/Internal/ForeignTypes.hs b/src/Crypto/Secp256k1/Internal/ForeignTypes.hs
--- a/src/Crypto/Secp256k1/Internal/ForeignTypes.hs
+++ b/src/Crypto/Secp256k1/Internal/ForeignTypes.hs
@@ -1,40 +1,66 @@
+-- | Types and constants shared by the foreign imports.
+--
+-- Each empty data type stands for a C structure of libsecp256k1 and is only
+-- ever used behind a 'Ptr', so that the foreign imports cannot mix up, say, a
+-- pointer to a public key and a pointer to a signature. The number in the name
+-- is the size of the structure in bytes, which the Haskell side must allocate.
+-- This module is internal and has no API stability guarantee.
 module Crypto.Secp256k1.Internal.ForeignTypes where
 
 import Foreign (Ptr)
 import Foreign.C (CInt (..), CUChar, CUInt (..))
 
+-- | Context (@secp256k1_context@).
 data LCtx
 
+-- | Public key (@secp256k1_pubkey@), 64 bytes.
 data PubKey64
 
+-- | Message hash, 32 bytes.
 data Msg32
 
+-- | ECDSA signature (@secp256k1_ecdsa_signature@), 64 bytes.
 data Sig64
 
+-- | Randomization seed for a context, 32 bytes.
 data Seed32
 
+-- | Compact ECDSA signature, r and s, 64 bytes.
 data Compact64
 
+-- | Secret key, 32 bytes.
 data SecKey32
 
+-- | Tweak, 32 bytes.
 data Tweak32
 
+-- | Recoverable ECDSA signature (@secp256k1_ecdsa_recoverable_signature@), 65 bytes.
 data RecSig65
 
+-- | X-only public key (@secp256k1_xonly_pubkey@), 64 bytes.
 data XOnlyPubKey64
 
+-- | Key pair (@secp256k1_keypair@), 96 bytes.
 data KeyPair96
 
+-- | BIP-340 signature, 64 bytes.
 data SchnorrSig64
 
+-- | Auxiliary randomness for BIP-340 signing, 32 bytes.
 data AuxRand32
 
+-- | Flags passed to @secp256k1_context_create@.
 type CtxFlags = CUInt
 
+-- | Flags passed to @secp256k1_ec_pubkey_serialize@.
 type SerFlags = CUInt
 
+-- | Return value of a C function: 1 for success, 0 for failure.
 type Ret = CInt
 
+-- | Nonce generation function, as @secp256k1_nonce_function@. The bindings pass
+-- 'Foreign.nullFunPtr' for the default RFC 6979 function, or a pointer to
+-- Bitcoin Core's grinding data through the last argument.
 type NonceFun a =
   Ptr CUChar ->
   Ptr CUChar ->
@@ -44,12 +70,17 @@
   CInt ->
   IO CInt
 
+-- | @SECP256K1_EC_COMPRESSED@: serialize a public key in 33 bytes.
 compressed :: SerFlags
 compressed = 0x0102
 
+-- | @SECP256K1_EC_UNCOMPRESSED@: serialize a public key in 65 bytes.
 uncompressed :: SerFlags
 uncompressed = 0x0002
 
+-- | Interpret a return value. Anything but 0 or 1 means the C library and these
+-- bindings disagree about a function's contract, so it raises an error rather
+-- than guess.
 isSuccess :: Ret -> Bool
 isSuccess 0 = False
 isSuccess 1 = True
diff --git a/src/Crypto/Secp256k1/Internal/RecoveryOps.hs b/src/Crypto/Secp256k1/Internal/RecoveryOps.hs
--- a/src/Crypto/Secp256k1/Internal/RecoveryOps.hs
+++ b/src/Crypto/Secp256k1/Internal/RecoveryOps.hs
@@ -18,6 +18,7 @@
 import Foreign (FunPtr, Ptr)
 import Foreign.C (CInt (..))
 
+-- | Binding to @secp256k1_ecdsa_recoverable_signature_parse_compact@.
 foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_parse_compact"
   ecdsaRecoverableSignatureParseCompact ::
     Ptr LCtx ->
@@ -27,6 +28,7 @@
     CInt ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_recoverable_signature_serialize_compact@.
 foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_serialize_compact"
   ecdsaRecoverableSignatureSerializeCompact ::
     Ptr LCtx ->
@@ -36,6 +38,7 @@
     Ptr RecSig65 ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_recoverable_signature_convert@.
 foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recoverable_signature_convert"
   ecdsaRecoverableSignatureConvert ::
     Ptr LCtx ->
@@ -45,6 +48,7 @@
     Ptr RecSig65 ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_sign_recoverable@.
 foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_sign_recoverable"
   ecdsaSignRecoverable ::
     Ptr LCtx ->
@@ -56,6 +60,7 @@
     Ptr a ->
     IO Ret
 
+-- | Binding to @secp256k1_ecdsa_recover@.
 foreign import ccall safe "secp256k1_recovery.h secp256k1_ecdsa_recover"
   ecdsaRecover ::
     Ptr LCtx ->
diff --git a/src/Crypto/Secp256k1/Internal/Util.hs b/src/Crypto/Secp256k1/Internal/Util.hs
--- a/src/Crypto/Secp256k1/Internal/Util.hs
+++ b/src/Crypto/Secp256k1/Internal/Util.hs
@@ -1,5 +1,13 @@
 {-# LANGUAGE FlexibleContexts #-}
 
+-- | Helpers to move bytes between Haskell and the C library safely.
+--
+-- Most mistakes in C bindings are about memory: passing a buffer of the wrong
+-- size, letting the C side write into a 'ByteString' that other code shares,
+-- or leaking a buffer when an exception is thrown. Every binding goes through
+-- these functions, which check lengths before calling C, give the C library
+-- fresh buffers to write into, and free them on every path. This module is
+-- internal and has no API stability guarantee.
 module Crypto.Secp256k1.Internal.Util where
 
 import Control.Exception (onException)
@@ -14,20 +22,26 @@
 import Foreign.C (CSize (..))
 import System.Entropy (getEntropy)
 
+-- | Decode hex, or 'Nothing' if the string is not valid hex.
 decodeHex :: (ConvertibleStrings a ByteString) => a -> Maybe ByteString
 decodeHex str =
   case parseBase16 $ cs str of
     Right b16 -> Just $ decodeBase16 b16
     Left _ -> Nothing
 
+-- | Show bytes as a quoted hex string.
 showsHex :: ByteString -> ShowS
 showsHex = shows . extractBase16 . encodeBase16
 
+-- | Pass the bytes of a 'ByteString' to C without copying them. Only for
+-- arguments that the C library reads and never writes: writing would change a
+-- 'ByteString' that may be shared.
 unsafeUseByteString :: ByteString -> ((Ptr a, CSize) -> IO b) -> IO b
 unsafeUseByteString bs f =
   BU.unsafeUseAsCStringLen bs $ \(b, l) ->
     f (castPtr b, fromIntegral l)
 
+-- | Pass a copy of the bytes of a 'ByteString' to C.
 useByteString :: ByteString -> ((Ptr a, CSize) -> IO b) -> IO b
 useByteString bs f =
   BS.useAsCStringLen bs $ \(b, l) ->
@@ -82,14 +96,17 @@
       copyBytes dst (castPtr src) len
       f dst
 
+-- | Take ownership of a @malloc@'ed buffer as a 'ByteString', without copying.
 unsafePackByteString :: (Ptr a, CSize) -> IO ByteString
 unsafePackByteString (b, l) =
   BU.unsafePackMallocCStringLen (castPtr b, fromIntegral l)
 
+-- | Copy a C buffer into a new 'ByteString'.
 packByteString :: (Ptr a, CSize) -> IO ByteString
 packByteString (b, l) =
   BS.packCStringLen (castPtr b, fromIntegral l)
 
+-- | Pass 32 bytes from the operating system's secure random source to C.
 withRandomSeed :: (Ptr Seed32 -> IO a) -> IO a
 withRandomSeed go = do
   bs <- getEntropy 32
