diff --git a/app/Main.hs b/app/Main.hs
index 176fb0d6e28599856d8ad93642b508f6d8f4005c..e07bda381ed0b08c5ec090a0ade5f7d4d2cf3f49 100644
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,12 +1,13 @@
 module Main where
 
--- import Data.Aeson
 import qualified Data.ByteString as B
+import qualified Data.Map.Strict as Map
 import Data.Text
-import Data.Yaml (decodeEither')
+import Data.Yaml (FromJSON (parseJSON), Value (Object), decodeEither', (.:))
 import System.Environment (getArgs)
+import Tahoe.Announcement (StorageServerAnnouncement, StorageServerID)
 import Tahoe.CHK.Capability
-import Tahoe.Download (download, gbsURLToStorageServer)
+import Tahoe.Download (announcementToStorageServer, download)
 import Text.Megaparsec (parse)
 
 main :: IO ()
@@ -14,20 +15,24 @@ main = do
     [announcementPath, readCap] <- getArgs
     -- Load server announcements
     announcementsBytes <- B.readFile announcementPath
-
-    -- XXX announcementsBytes is not quite the right shape for `Map
-    -- StorageServerID StorageServerAnnouncement` - need to massage it a
-    -- little bit somehow?
-    -- let Just yaml = decodeEither' announcementBytes :: Either ParseException (Maybe Value)
-    let Right announcements = decodeEither' announcementsBytes
+    let Right (Announcements announcements) = decodeEither' announcementsBytes
+    print ("Your announcements:" :: Data.Text.Text)
+    print announcements
 
     -- Accept & parse read capability
     let Right (CHKReader cap) = parse pCapability "<argv>" (Data.Text.pack readCap)
 
-    -- Download the shares
-    -- Decode them
-    Right plaintext <- download announcements cap gbsURLToStorageServer
+    -- Download & decode the shares
+    result <- download announcements cap announcementToStorageServer
 
     -- Show the result
-    print ("Your plaintext:" :: Data.Text.Text)
-    print plaintext
+    print ("Your result:" :: Data.Text.Text)
+    print result
+
+newtype Announcements = Announcements (Map.Map StorageServerID StorageServerAnnouncement)
+
+instance FromJSON Announcements where
+    parseJSON (Object v) = do
+        storage <- v .: "storage"
+        pure $ Announcements storage
+    parseJSON _ = error "unsupported announcements format"
diff --git a/gbs-downloader.cabal b/gbs-downloader.cabal
index 2459c151c7fafe2b0749c0cec065a6da07f5c89d..c462d6915dfec014a811cb7695d12d012b9507ff 100644
--- a/gbs-downloader.cabal
+++ b/gbs-downloader.cabal
@@ -75,7 +75,9 @@ library
     , language
 
   -- Modules exported by the library.
-  exposed-modules:  Tahoe.Download
+  exposed-modules:
+    Tahoe.Announcement
+    Tahoe.Download
 
   -- Modules included in this library but not exported.
   -- other-modules:
@@ -85,6 +87,7 @@ library
 
   -- Other library packages from which modules are imported.
   build-depends:
+    , aeson
     , base                     ^>=4.14.3.0
     , base32
     , binary
@@ -98,6 +101,7 @@ library
     , tahoe-chk
     , tahoe-great-black-swamp
     , text
+    , yaml
 
   -- Directories containing source files.
   hs-source-dirs:   src
@@ -125,6 +129,7 @@ executable gbs-download
     , aeson
     , base            ^>=4.14.3.0
     , bytestring
+    , containers
     , gbs-downloader
     , megaparsec
     , tahoe-chk
diff --git a/src/Tahoe/Download.hs b/src/Tahoe/Download.hs
index 0ff1e5c45332d28214662c670e2cc2b1d2585a64..f25bac673470c6b64a9dcd5aace7333348596ed2 100644
--- a/src/Tahoe/Download.hs
+++ b/src/Tahoe/Download.hs
@@ -7,29 +7,32 @@
 module Tahoe.Download (
     LookupServer,
     DownloadError (..),
+    LookupError (..),
+    DiscoverError (..),
     discoverShares,
     download,
-    gbsURLToStorageServer,
+    announcementToStorageServer,
 ) where
 
 import Control.Exception (throwIO)
 import Control.Monad.IO.Class (MonadIO (liftIO))
 import Data.Binary (decodeOrFail)
 import qualified Data.ByteString.Lazy as LB
-import Data.Either (isRight, rights)
+import Data.Either (isRight, partitionEithers)
 import Data.List (foldl')
 import qualified Data.Map.Strict as Map
 import qualified Data.Set as Set
 import qualified Data.Text as T
 import Data.Text.Encoding (decodeUtf8)
 import Network.HTTP.Client (ManagerSettings, defaultManagerSettings, newManager)
-import Network.URI (URI (..), URIAuth (..), parseURI)
+import Network.URI (URI (..), URIAuth (..))
 import Servant.Client (Scheme (Https), mkClientEnv, runClientM)
 import Servant.Client.Core (BaseUrl (BaseUrl, baseUrlHost, baseUrlPath, baseUrlPort, baseUrlScheme))
+import Tahoe.Announcement (StorageServerAnnouncement, greatBlackSwampURIs)
 import qualified Tahoe.CHK
 import Tahoe.CHK.Capability (Reader (..), Verifier (..))
 import qualified Tahoe.CHK.Encrypt
-import Tahoe.CHK.Server (StorageServer (..), StorageServerAnnouncement (..), StorageServerID, URL)
+import Tahoe.CHK.Server (StorageServer (..), StorageServerID)
 import Tahoe.CHK.Types (ShareNum, StorageIndex)
 import TahoeLAFS.Storage.API (ShareNumber (ShareNumber))
 import TahoeLAFS.Storage.Client (readImmutableShares)
@@ -42,7 +45,7 @@ data DownloadError
     = -- | The configuration included no candidate servers from which to download.
       NoConfiguredServers
     | -- | Across all of the configured servers, none were actually connectable.
-      NoReachableServers
+      NoReachableServers [DiscoverError]
     | -- | Across all of the configured servers, fewer than the required
       -- number of shares were found. XXX Could split this into the different
       -- cases - did not locate enough shares, did not download enough shares,
@@ -73,14 +76,14 @@ data DiscoverError
       StorageServerLocationUnknown
     | -- | An announcement included a location we could not interpret.
       StorageServerLocationUnsupported
-    | StorageServerUnreachable
+    | StorageServerUnreachable LookupError
     deriving (Eq, Ord, Show)
 
 -- TODO The result might need to be in IO in case the URL indicates a
 -- Tor-based route to the server.  In this case we might need to launch a Tor
 -- daemon or connect to a running Tor daemon or at least set up a new Tor
 -- circuit.  All of which require I/O.  But we can always refactor later!
-type LookupServer m = URL -> m (Maybe StorageServer)
+type LookupServer m = StorageServerAnnouncement -> m (Either LookupError StorageServer)
 
 {- | Recover the application data associated with a given capability from the
  given servers, if possible.
@@ -102,9 +105,12 @@ download servers cap@Reader{readKey, verifier = Verifier{..}} lookupServer =
         [] -> pure . Left $ NoConfiguredServers
         serverList -> do
             -- Ask each server for all shares it has.
-            (discovered :: [(StorageServer, Set.Set ShareNum)]) <- rights <$> mapM (discoverShares lookupServer storageIndex) serverList
+            ( problems :: [DiscoverError]
+                , discovered :: [(StorageServer, Set.Set ShareNum)]
+                ) <-
+                partitionEithers <$> mapM (discoverShares lookupServer storageIndex) serverList
             if null discovered
-                then pure $ Left NoReachableServers
+                then pure . Left . NoReachableServers $ problems
                 else
                     if (fromIntegral required >) . countDistinctShares $ discovered
                         then pure $ Left NotEnoughShares{notEnoughSharesNeeded = fromIntegral required, notEnoughSharesFound = countDistinctShares discovered}
@@ -143,14 +149,11 @@ discoverShares ::
     (StorageServerID, StorageServerAnnouncement) ->
     m (Either DiscoverError (StorageServer, Set.Set ShareNum))
 discoverShares lookupServer storageIndex (_sid, sann) = do
-    case storageServerAnnouncementFURL sann of
-        Nothing -> pure $ Left StorageServerLocationUnknown
-        Just url -> do
-            server <- lookupServer url
-            case server of
-                Nothing -> pure $ Left StorageServerUnreachable
-                Just ss@StorageServer{storageServerGetBuckets} ->
-                    liftIO $ Right . (ss,) <$> storageServerGetBuckets storageIndex
+    server <- lookupServer sann
+    case server of
+        Left e -> pure . Left . StorageServerUnreachable $ e
+        Right ss@StorageServer{storageServerGetBuckets} ->
+            liftIO $ Right . (ss,) <$> storageServerGetBuckets storageIndex
 
 {- | Invert the mapping implied by the list of two tuples so that the servers
  that claim to have a certain share can easily be retrieved.
@@ -182,13 +185,19 @@ downloadShare storageIndex shareNum (s : _) = do
     shareBytes <- liftIO $ storageServerRead s storageIndex shareNum
     pure (shareNum, Right $ LB.fromStrict shareBytes)
 
-gbsURLToStorageServer :: MonadIO m => T.Text -> m (Maybe StorageServer)
-gbsURLToStorageServer url =
-    case parseURI (T.unpack url) of
-        Nothing -> pure Nothing
+data LookupError
+    = URIParseError StorageServerAnnouncement
+    | PortParseError String
+    | AnnouncementStructureUnmatched
+    deriving (Eq, Ord, Show)
+
+announcementToStorageServer :: MonadIO m => StorageServerAnnouncement -> m (Either LookupError StorageServer)
+announcementToStorageServer ann =
+    case greatBlackSwampURIs ann of
+        Nothing -> pure . Left . URIParseError $ ann
         Just uri -> makeServer uri
 
-makeServer :: MonadIO m => URI -> m (Maybe StorageServer)
+makeServer :: MonadIO m => URI -> m (Either LookupError StorageServer)
 makeServer
     URI
         { uriScheme = "pb"
@@ -197,7 +206,7 @@ makeServer
         , uriFragment = "#v=1"
         } =
         case readMaybe port of
-            Nothing -> pure Nothing
+            Nothing -> pure . Left . PortParseError $ port
             Just realPort -> do
                 manager <- liftIO $ newManager (managerSettingsForService (T.pack tubid) (T.pack swissnum))
 
@@ -216,8 +225,8 @@ makeServer
                             Right bs -> pure bs
 
                     storageServerGetBuckets = undefined
-                pure $ Just StorageServer{..}
-makeServer _ = pure Nothing
+                pure . Right $ StorageServer{..}
+makeServer _ = pure . Left $ AnnouncementStructureUnmatched
 
 https :: String -> Int -> BaseUrl
 https host port =
diff --git a/test/Spec.hs b/test/Spec.hs
index bd8b7aacd430d140fe9eb67572345a1f96a2c96a..e056b04cdfa06cc2dd4ce9d58962d24de29d2c57 100644
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -18,13 +18,14 @@ import Hedgehog (MonadGen, diff, forAll, property)
 import qualified Hedgehog.Gen as Gen
 import qualified Hedgehog.Range as Range
 import System.IO (hSetEncoding, stderr, stdout, utf8)
+import Tahoe.Announcement (StorageServerAnnouncement (..), StorageServerID)
 import qualified Tahoe.CHK
 import Tahoe.CHK.Capability (Reader (..), Verifier (..))
 import qualified Tahoe.CHK.Encrypt
-import Tahoe.CHK.Server (StorageServer (..), StorageServerAnnouncement (..), StorageServerID)
+import Tahoe.CHK.Server (StorageServer (..))
 import Tahoe.CHK.Types (Parameters (..))
 import Tahoe.CHK.Upload (getConvergentKey)
-import Tahoe.Download (DownloadError (..), LookupServer, download)
+import Tahoe.Download (DiscoverError (StorageServerUnreachable), DownloadError (..), LookupError (..), LookupServer, download)
 import Tahoe.Server (memoryStorageServer)
 import Test.Tasty (TestTree, defaultMain, testGroup)
 import Test.Tasty.HUnit (assertEqual, testCase)
@@ -48,22 +49,24 @@ tests =
         , testCase "no reachable servers" $ do
             -- If we can't contact any configured server then we can't
             -- possibly get enough shares to recover the application data.
-            let anns =
+            let ann =
+                    StorageServerAnnouncement
+                        { storageServerAnnouncementFURL = Nothing
+                        , storageServerAnnouncementNick = Just "unreachable"
+                        , storageServerAnnouncementPermutationSeed = Nothing
+                        }
+                anns =
                     Map.fromList
                         [
                             ( "v0-abc123"
-                            , StorageServerAnnouncement
-                                { storageServerAnnouncementFURL = Nothing
-                                , storageServerAnnouncementNick = Just "unreachable"
-                                , storageServerAnnouncementPermutationSeed = Nothing
-                                }
+                            , ann
                             )
                         ]
 
             result <- liftIO $ download anns (trivialCap 1 1) noServers
             assertEqual
                 "download should fail with no reachable servers"
-                (Left NoReachableServers)
+                (Left $ NoReachableServers [StorageServerUnreachable (URIParseError ann)])
                 result
         , testCase "not enough shares" $ do
             -- If we can't recover enough shares from the configured servers
@@ -88,10 +91,10 @@ tests =
             storageServerWrite server (storageIndex . verifier $ cap) 1 0 "Hello world"
 
             -- Make the server reachable.
-            let openServer furl =
-                    if furl == "somewhere"
+            let openServer StorageServerAnnouncement{storageServerAnnouncementFURL} =
+                    if storageServerAnnouncementFURL == Just "somewhere"
                         then pure . pure $ server
-                        else pure Nothing
+                        else pure . Left $ AnnouncementStructureUnmatched
 
             -- Try to download the cap which requires three shares to reconstruct.
             result <- liftIO $ download anns cap openServer
@@ -136,11 +139,11 @@ tests =
             storageServerWrite elsewhere idx 0 offset "Hello world"
 
             -- Make the server reachable.
-            let openServer furl =
-                    case furl of
-                        "somewhere" -> pure . pure $ somewhere
-                        "elsewhere" -> pure . pure $ elsewhere
-                        _ -> pure Nothing
+            let openServer StorageServerAnnouncement{storageServerAnnouncementFURL} =
+                    case storageServerAnnouncementFURL of
+                        Just "somewhere" -> pure . pure $ somewhere
+                        Just "elsewhere" -> pure . pure $ elsewhere
+                        _ -> pure . Left $ AnnouncementStructureUnmatched
 
             -- Try to download the cap which requires three shares to reconstruct.
             result <- liftIO $ download anns cap openServer
@@ -221,12 +224,20 @@ tests =
         ]
   where
     -- A server lookup function that always fails.
-    noServers _ = pure Nothing
+    noServers = pure . Left . URIParseError
 
     -- A server lookup function that finds servers already present in a Map.
     someServers :: Applicative m => Map.Map StorageServerID StorageServer -> LookupServer m
-    someServers servers = pure . flip Map.lookup servers . parseURL
+    someServers servers ann =
+        pure $ case result of
+            Nothing -> Left AnnouncementStructureUnmatched
+            Just ss -> Right ss
       where
+        result = do
+            furl <- storageServerAnnouncementFURL ann
+            let serverId = parseURL furl
+            Map.lookup serverId servers
+
         -- Exactly match the nonsense makeAnn spits out
         parseURL = T.take 2 . T.drop 5