diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,57 @@
+# indieweb-algorithms [![Hackage](https://img.shields.io/hackage/v/indieweb-algorithms.svg?style=flat)](https://hackage.haskell.org/package/indieweb-algorithms) [![Build Status](https://img.shields.io/travis/myfreeweb/indieweb-algorithms.svg?style=flat)](https://travis-ci.org/myfreeweb/indieweb-algorithms) [![unlicense](https://img.shields.io/badge/un-license-green.svg?style=flat)](http://unlicense.org)
+
+A collection of implementations of [IndieWeb]- and [Microformats 2]-related algorithms (based on [microformats2-parser] and [http-link-header]):
+
+- finding all microformats of a given type (while retaining the path to them), ie. flattening the tree
+- discovering **[authorship](http://indiewebcamp.com/authorship)** of an `h-entry`
+- discovering [Webmention](http://indiewebcamp.com/Webmention)/[Micropub](http://indiewebcamp.com/Micropub)/[IndieAuth](http://indiewebcamp.com/IndieAuth)/etc. **endpoints** (HTTP `Link` header, `a` and `link` tags with the `rel` attribute)
+
+[#IndieWeb]: http://indiewebcamp.com
+[Microformats 2]: http://microformats.org/wiki/microformats2
+[microformats2-parser]: https://github.com/myfreeweb/microformats2-parser
+[http-link-header]: https://github.com/myfreeweb/http-link-header
+
+## Usage
+
+### Endpoints
+
+```haskell
+{-# LANGUAGE OverloadedStrings #-}
+
+import Network.HTTP.Link
+import Data.Default
+import Data.Maybe
+import Data.Microformats2.Parser
+import Data.IndieWeb.Endpoints
+
+discoverEndpoints [ "micropub" ] (parseMf2 def $ documentRoot $ parseLBS "<link rel=micropub href='http://example.com/micropub2'>...") (fromMaybe [] $ parseLinkHeader "<http://example.com/micropub>; rel=\"micropub\"")
+```
+
+## Development
+
+Use [stack] to build.  
+Use ghci to run tests quickly with `:test` (see the `.ghci` file).
+
+```bash
+$ stack build
+
+$ stack test && rm tests.tix
+
+$ stack ghci --ghc-options="-fno-hpc"
+```
+
+[stack]: https://github.com/commercialhaskell/stack
+
+## Contributing
+
+Please feel free to submit pull requests!
+Bugfixes and simple non-breaking improvements will be accepted without any questions :-)
+
+By participating in this project you agree to follow the [Contributor Code of Conduct](http://contributor-covenant.org/version/1/2/0/).
+
+[The list of contributors is available on GitHub](https://github.com/myfreeweb/indieweb-algorithms/graphs/contributors).
+
+## License
+
+This is free and unencumbered software released into the public domain.  
+For more information, please refer to the `UNLICENSE` file or [unlicense.org](http://unlicense.org).
diff --git a/UNLICENSE b/UNLICENSE
new file mode 100644
--- /dev/null
+++ b/UNLICENSE
@@ -0,0 +1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
diff --git a/indieweb-algorithms.cabal b/indieweb-algorithms.cabal
new file mode 100644
--- /dev/null
+++ b/indieweb-algorithms.cabal
@@ -0,0 +1,76 @@
+name:            indieweb-algorithms
+version:         0.1.0
+synopsis:        A collection of implementations of IndieWeb algorithms.
+category:        Web
+homepage:        https://github.com/myfreeweb/indieweb-algorithms
+author:          Greg V
+copyright:       2015 Greg V <greg@unrelenting.technology>
+maintainer:      greg@unrelenting.technology
+license:         PublicDomain
+license-file:    UNLICENSE
+build-type:      Simple
+cabal-version:   >= 1.18
+extra-source-files:
+    README.md
+tested-with:
+    GHC == 7.10.2
+
+
+source-repository head
+    type: git
+    location: git://github.com/myfreeweb/indieweb-algorithms.git
+
+library
+    build-depends:
+        base >= 4.0.0.0 && < 5
+      , transformers
+      , text
+      , bytestring
+      , time
+      , either
+      , safe
+      , containers
+      , unordered-containers
+      , vector
+      , lens
+      , aeson
+      , lens-aeson
+      , data-default
+      , network-uri
+      , microformats2-parser
+      , http-link-header
+    default-language: Haskell2010
+    exposed-modules:
+        Data.IndieWeb.Authorship
+        Data.IndieWeb.Endpoints
+        Data.IndieWeb.MicroformatsUtil
+    ghc-options: -Wall
+    hs-source-dirs: library
+
+test-suite tests
+    build-depends:
+        base >= 4.0.0.0 && < 5
+      , mtl
+      , time
+      , text
+      , bytestring
+      , network-uri
+      , hspec
+      , hspec-expectations-pretty-diff
+      , aeson
+      , template-haskell
+      , indieweb-algorithms
+      , microformats2-parser
+      , raw-strings-qq
+      , aeson-qq
+      , data-default
+    default-language: Haskell2010
+    ghc-options: -threaded -Wall -fhpc
+    hs-source-dirs: test-suite
+    main-is: Spec.hs
+    other-modules:
+        TestCommon
+        Data.IndieWeb.AuthorshipSpec
+        Data.IndieWeb.EndpointsSpec
+        Data.IndieWeb.MicroformatsUtilSpec
+    type: exitcode-stdio-1.0
diff --git a/library/Data/IndieWeb/Authorship.hs b/library/Data/IndieWeb/Authorship.hs
new file mode 100644
--- /dev/null
+++ b/library/Data/IndieWeb/Authorship.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE OverloadedStrings, UnicodeSyntax, CPP, FlexibleContexts #-}
+
+module Data.IndieWeb.Authorship where
+
+#if __GLASGOW_HASKELL__ < 709
+import           Control.Applicative
+#endif
+import           Control.Monad
+import           Control.Monad.Trans.Maybe
+import           Control.Lens
+import qualified Data.Text as T
+import qualified Data.ByteString.Lazy as LB
+import qualified Data.Vector as V
+import           Data.Foldable (asum)
+import           Data.Maybe
+import           Data.Aeson
+import           Data.Aeson.Lens
+import           Data.Microformats2.Parser
+import           Data.IndieWeb.MicroformatsUtil
+import           Network.URI
+import           Safe (headMay)
+
+-- | Finds the authors of an h-entry, discovering its authorship <http://indiewebcamp.com/authorship> using the HTTP fetcher function from arguments.
+entryAuthors ∷ Monad μ ⇒ Mf2ParserSettings
+                       → (URI → μ (Maybe LB.ByteString)) -- ^ the URI fetcher function
+                       → URI -- ^ the URI of the page the entry was extracted from
+                       → Value -- ^ the full Microformats 2 parse of the page as extracted by 'Data.Microformats2.Parser.parseMf2'
+                       → (Value, [Value]) -- ^ (the h-entry, [parent microformats]) as extracted by 'Data.IndieWeb.MicroformatsUtil.allMicroformatsOfType'
+                       → μ (Maybe [Value])
+entryAuthors mfSettings fetch entryUri mfRoot (entry, parents) = runMaybeT $ asum $ map MaybeT [ embeddedCards, relCards ]
+  where fetchIfLink v
+          | isMf "h-card" v = return $ Just v
+          | otherwise = case (T.unpack <$> v ^? _String) >>= parseURIReference of
+                          Nothing → return $ Just v
+                          Just uri → cardFromUri uri
+        embeddedCards = case asum [ entryAuthor, feedAuthor ] of
+                          Nothing → return Nothing
+                          Just authors → liftM (Just . catMaybes) $ mapM fetchIfLink authors
+        relCards = case V.toList <$> mfRoot ^? key "rels" . key "author" . _Array of
+                     Nothing → return Nothing
+                     Just rels → liftM (Just . catMaybes) $ mapM fetchIfLink rels
+        entryAuthor = getAuthorProp entry
+        feedAuthor = getAuthorProp =<< (headMay $ filter (isMf "h-feed") parents)
+        getAuthorProp = (V.toList <$>) . (^? key "properties" . key "author" . _Array)
+        isMf t = (String t `V.elem`) . (fromMaybe V.empty) . (^? key "type" . _Array)
+        cardFromUri uri = do
+          -- TODO: only allow http(s)
+          html ← fetch $ uri `relativeTo` entryUri
+          -- TODO: representative h-card, not just first
+          return $ fmap fst $ headMay =<< allMicroformatsOfType "h-card" =<< parseMf2 mfSettings <$> documentRoot <$> parseLBS <$> html
diff --git a/library/Data/IndieWeb/Endpoints.hs b/library/Data/IndieWeb/Endpoints.hs
new file mode 100644
--- /dev/null
+++ b/library/Data/IndieWeb/Endpoints.hs
@@ -0,0 +1,22 @@
+{-# LANGUAGE OverloadedStrings, UnicodeSyntax #-}
+
+module Data.IndieWeb.Endpoints where
+
+import           Control.Lens
+import           Data.Maybe (catMaybes)
+import           Data.List (nub)
+import           Data.Aeson
+import           Data.Aeson.Lens
+import qualified Data.Text as T
+import qualified Data.Vector as V
+import           Network.URI
+import           Network.HTTP.Link
+
+discoverEndpoints ∷ [T.Text]-- ^ the rels of the links you want to find (alternatives, like "webmention" and "http://webmention.org/")
+                  → Value -- ^ the full Microformats 2 parse of the page as extracted by 'Data.Microformats2.Parser.parseMf2'
+                  → [Link] -- ^ the Link header as parsed by 'Network.HTTP.Link.parseLinkHeader'
+                  → [URI]
+discoverEndpoints rels mfRoot linkH = nub $ headerLinks ++ mfLinks
+  where headerLinks = map href $ concat $ map (\r → filter (\(Link _ as) → (Rel, r) `elem` as) linkH) rels
+        mfLinks = catMaybes $ map parseAesonLink $ concat $ catMaybes $ map (\r → V.toList <$> mfRoot ^? key "rels" . key r . _Array) rels
+        parseAesonLink v = (T.unpack <$> v ^? _String) >>= parseURI
diff --git a/library/Data/IndieWeb/MicroformatsUtil.hs b/library/Data/IndieWeb/MicroformatsUtil.hs
new file mode 100644
--- /dev/null
+++ b/library/Data/IndieWeb/MicroformatsUtil.hs
@@ -0,0 +1,27 @@
+{-# LANGUAGE OverloadedStrings, UnicodeSyntax #-}
+
+module Data.IndieWeb.MicroformatsUtil where
+
+import           Control.Lens
+import           Data.Maybe
+import           Data.Aeson
+import           Data.Aeson.Lens
+import qualified Data.Vector as V
+import qualified Data.HashMap.Strict as HMS
+import qualified Data.Text as T
+
+-- | Recursively finds microformats of a given type, also keeping the path (in reverse order) that led to them.
+-- Returns Nothing when the given Value isn't an object with an 'items' key, ie. is not a result of Microformats 2 parsing.
+allMicroformatsOfType ∷ T.Text → Value → Maybe [(Value, [Value])]
+allMicroformatsOfType typeName mf = do
+  items ← mf ^? key "items" . _Array
+  let findOfType acc path v =
+        let valToList (Array vec) = V.toList vec
+            valToList _ = []
+            childMfs = V.toList $ fromMaybe V.empty $ v ^? key "children" . _Array
+            propMfs = concatMap valToList $ HMS.elems $ fromMaybe HMS.empty $ v ^? key "properties" . _Object
+            acc' = acc ++ concatMap (findOfType acc $ v : path) (childMfs ++ propMfs) in
+        if String typeName `V.elem` fromMaybe V.empty (v ^? key "type" . _Array)
+           then (v, path) : acc'
+           else acc'
+  Just $ concatMap (findOfType [] []) items
diff --git a/test-suite/Data/IndieWeb/AuthorshipSpec.hs b/test-suite/Data/IndieWeb/AuthorshipSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Data/IndieWeb/AuthorshipSpec.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE QuasiQuotes, OverloadedStrings, UnicodeSyntax #-}
+
+module Data.IndieWeb.AuthorshipSpec (spec) where
+
+import           Test.Hspec hiding (shouldBe)
+import           Test.Hspec.Expectations.Pretty (shouldBe)
+import           TestCommon
+import           Network.URI
+import qualified Data.ByteString.Lazy as LB
+import           Data.Functor.Identity
+import           Data.Maybe
+import           Data.Aeson
+import           Data.Microformats2.Parser
+import           Data.IndieWeb.Authorship
+import           Data.IndieWeb.MicroformatsUtil
+
+spec ∷ Spec
+spec = do
+  describe "findAuthors" $ do
+    let o = object
+        mockFetch ∷ URI → Identity (Maybe LB.ByteString)
+        mockFetch "http://direct" = return $ Just [xml|<body><div class="h-entry"> <data class=p-name value=Name> <div class="p-author h-card"><h1 class="p-name">Author from Direct!|]
+        mockFetch "http://link" = return $ Just [xml|<body><div class="h-entry"> <data class=p-name value=Name> <a class="p-author">http://author/page</a>|]
+        mockFetch "http://rel" = return $ Just [xml|<body> <a href="http://author/page" rel="author"></a><div class="h-entry"><data class=p-name value=Name>|]
+        mockFetch "http://author/page/link-relative" = return $ Just [xml|<body><div class="h-entry"> <data class=p-name value=Name> <a class="p-author">/page</a>|]
+        mockFetch "http://feed" = return $ Just [xml|<body><div class="h-feed"> <div class="p-author h-card"><h1 class="p-name">Author from Feed!</h1></div><div class="h-entry"><data class=p-name value=Name>|]
+        mockFetch "http://feed/link" = return $ Just [xml|<body><div class="h-feed"> <a class="p-author">http://author/page</a><div class="h-entry"><data class=p-name value=Name>|]
+        mockFetch "http://author/page" = return $ Just [xml|<body><div class="h-card"><h1 class="p-name">Author from Page!|]
+        mockFetch _ = return Nothing
+        rE u = let mf = parseMf2 def . documentRoot . parseLBS . fromMaybe "" . runIdentity $ mockFetch u in
+                   runIdentity . entryAuthors def mockFetch u mf . head . fromJust $ allMicroformatsOfType "h-entry" mf
+        card n = o [ "value" .= String n
+                   , "type" .= [ String "h-card" ]
+                   , "properties" .= o [ "name" .= [ String n ] ] ]
+        card' n = o [ "type" .= [ String "h-card" ]
+                    , "properties" .= o [ "name" .= [ String n ] ] ]
+
+    it "finds the author h-cards embedded in the h-entry" $ do
+      rE "http://direct" `shouldBe` Just [ card "Author from Direct!" ]
+
+    it "finds the author h-cards embedded in the h-feed parent of h-entry" $ do
+      rE "http://feed" `shouldBe` Just [ card "Author from Feed!" ]
+
+    it "finds the author h-cards linked from the h-entry" $ do
+      rE "http://link" `shouldBe` Just [ card' "Author from Page!" ]
+
+    it "finds the author h-cards linked from the h-feed parent of h-entry" $ do
+      rE "http://feed/link" `shouldBe` Just [ card' "Author from Page!" ]
+
+    it "finds the author h-cards linked as rel=author" $ do
+      rE "http://rel" `shouldBe` Just [ card' "Author from Page!" ]
+      rE "http://author/page/link-relative" `shouldBe` Just [ card' "Author from Page!" ]
diff --git a/test-suite/Data/IndieWeb/EndpointsSpec.hs b/test-suite/Data/IndieWeb/EndpointsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Data/IndieWeb/EndpointsSpec.hs
@@ -0,0 +1,31 @@
+{-# LANGUAGE QuasiQuotes, OverloadedStrings, UnicodeSyntax #-}
+
+module Data.IndieWeb.EndpointsSpec (spec) where
+
+import           Test.Hspec hiding (shouldBe)
+import           Test.Hspec.Expectations.Pretty (shouldBe)
+import           TestCommon()
+import           Network.HTTP.Link
+import           Data.Aeson
+import           Data.IndieWeb.Endpoints
+
+spec ∷ Spec
+spec = do
+  describe "discoverEndpoint" $ do
+    let dW = discoverEndpoints [ "webmention", "http://webmention.org/" ]
+        o = object
+
+    it "finds endpoints in mf2" $ do
+      dW (o [ "rels" .= o [ "webmention" .= [ String "http://a/wm1", String "http://a/wm2" ]
+                          , "http://webmention.org/" .= [ String "http://a/wm3" ]
+                          , "nope" .= [ String "http://a/nope" ]] ]) [] `shouldBe` [ "http://a/wm1", "http://a/wm2", "http://a/wm3" ]
+
+    it "finds endpoints in the link header" $ do
+      dW (o []) [ Link "http://a/wm1" [(Rel, "webmention")]
+                , Link "http://a/wm2" [(Rel, "http://webmention.org/")] ] `shouldBe` [ "http://a/wm1", "http://a/wm2" ]
+
+    it "deduplicates links" $ do
+      dW (o [ "rels" .= o [ "webmention" .= [ String "http://a/wm3", String "http://a/wm4" ]
+                          , "http://webmention.org/" .= [ String "http://a/wm2" ]
+                          , "nope" .= [ String "http://a/nope" ]] ]) [ Link "http://a/wm1" [(Rel, "webmention")]
+                                                                     , Link "http://a/wm2" [(Rel, "http://webmention.org/")] ] `shouldBe` [ "http://a/wm1", "http://a/wm2", "http://a/wm3", "http://a/wm4" ]
diff --git a/test-suite/Data/IndieWeb/MicroformatsUtilSpec.hs b/test-suite/Data/IndieWeb/MicroformatsUtilSpec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Data/IndieWeb/MicroformatsUtilSpec.hs
@@ -0,0 +1,36 @@
+{-# LANGUAGE QuasiQuotes, OverloadedStrings, UnicodeSyntax #-}
+
+module Data.IndieWeb.MicroformatsUtilSpec (spec) where
+
+import           Test.Hspec hiding (shouldBe)
+import           Test.Hspec.Expectations.Pretty (shouldBe)
+import           TestCommon
+import           Data.Microformats2.Parser
+import           Data.IndieWeb.MicroformatsUtil
+
+spec ∷ Spec
+spec = do
+  describe "allMicroformatsOfType" $ do
+    it "recursively finds microformats in items" $ do
+      let mf = parseMf2 def $ documentRoot $ parseLBS [xml|<body>
+        <div class=h-entry>
+          <h1 class=p-name>Entry</h1>
+          <div class=h-child>
+            <data class=p-name value=Child>
+            <div class=h-grand-child>Grandchild</div>
+          </div>
+          <div class="p-prop h-prop-child">
+            <data class=p-name value=PropChild>
+            <div class=h-grand-child>PropGrandchild</div>
+          </div>
+        </div>|]
+          entryObj = [json|{"type":["h-entry"],"children":[{"value":"Child","type":["h-child"],"children":[{"value":"Grandchild","type":["h-grand-child"],"properties":{"name":["Grandchild"]}}],"properties":{"name":["Child"]}}],"properties":{"name":["Entry"],"prop":[{"value":"PropChild","type":["h-prop-child"],"children":[{"value":"PropGrandchild","type":["h-grand-child"],"properties":{"name":["PropGrandchild"]}}],"properties":{"name":["PropChild"]}}]}}|]
+          childObj = [json|{"value":"Child","type":["h-child"],"children":[{"value":"Grandchild","type":["h-grand-child"],"properties":{"name":["Grandchild"]}}],"properties":{"name":["Child"]}}|]
+          grandchildObj = [json|{"value":"Grandchild","type":["h-grand-child"],"properties":{"name":["Grandchild"]}}|]
+          propchildObj = [json|{"value":"PropChild","type":["h-prop-child"],"children":[{"value":"PropGrandchild","type":["h-grand-child"],"properties":{"name":["PropGrandchild"]}}],"properties":{"name":["PropChild"]}}|]
+          propgrandchildObj = [json|{"value":"PropGrandchild","type":["h-grand-child"],"properties":{"name":["PropGrandchild"]}}|]
+      allMicroformatsOfType "h-blah" mf `shouldBe` Just []
+      allMicroformatsOfType "h-entry" mf `shouldBe` Just [ (entryObj, []) ]
+      allMicroformatsOfType "h-child" mf `shouldBe` Just [ (childObj, [entryObj]) ]
+      allMicroformatsOfType "h-prop-child" mf `shouldBe` Just [ (propchildObj, [entryObj]) ]
+      allMicroformatsOfType "h-grand-child" mf `shouldBe` Just [ (grandchildObj, [childObj, entryObj]), (propgrandchildObj, [propchildObj, entryObj]) ]
diff --git a/test-suite/Spec.hs b/test-suite/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/test-suite/TestCommon.hs b/test-suite/TestCommon.hs
new file mode 100644
--- /dev/null
+++ b/test-suite/TestCommon.hs
@@ -0,0 +1,26 @@
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-# LANGUAGE UnicodeSyntax #-}
+
+module TestCommon (
+  xml
+, json
+, def
+, parseURI
+, parseURIReference
+) where
+
+import           Language.Haskell.TH.Quote
+import           Text.RawString.QQ (r)
+import           Network.URI (parseURI, parseURIReference, URI)
+import           Data.Aeson.QQ (aesonQQ)
+import           Data.Default (def)
+import           Data.String (IsString, fromString)
+import           Data.Maybe (fromJust)
+
+instance IsString URI where
+    fromString = fromJust . parseURIReference
+
+-- renames for vim
+xml, json ∷ QuasiQuoter
+xml = r
+json = aesonQQ
