diff --git a/NationStates.hs b/NationStates.hs
new file mode 100644
--- /dev/null
+++ b/NationStates.hs
@@ -0,0 +1,36 @@
+module NationStates (
+
+    Context(),
+    withContext,
+
+    -- * Core types
+    module NationStates.Types,
+
+    ) where
+
+
+import Network.HTTP.Client
+import Network.HTTP.Client.TLS
+
+import NationStates.Core
+import NationStates.RateLimit
+import NationStates.Types
+
+
+-- | Create a 'Context', and pass it to the provided function.
+--
+-- The 'Context' will be closed automatically when the function returns.
+withContext
+    :: String
+        -- ^ User agent
+    -> (Context -> IO a)
+    -> IO a
+withContext userAgent f = withManager tlsManagerSettings $ \man -> do
+    limit <- newRateLimit delay
+    f Context {
+        contextManager = man,
+        contextRateLimit = rateLimit limit,
+        contextUserAgent = userAgent
+        }
+  where
+    delay = 600 * 1000 * 1000  -- 0.6 seconds
diff --git a/NationStates/Core.hs b/NationStates/Core.hs
new file mode 100644
--- /dev/null
+++ b/NationStates/Core.hs
@@ -0,0 +1,222 @@
+{-# LANGUAGE OverloadedStrings, Rank2Types #-}
+
+-- | Low-level tools for querying the NationStates API.
+--
+-- Most of the time, you should use the high-level wrappers in e.g.
+-- "NationStates.Nation" instead. But if you need something not provided
+-- by these wrappers, then feel free to use this module directly.
+
+module NationStates.Core (
+
+    -- * Requests
+    NS,
+    makeNS,
+    makeNS',
+    requestNS,
+    apiVersion,
+
+    -- * Query strings
+    Query(..),
+
+    -- * Connection manager
+    Context(..),
+
+    -- * Utilities
+    splitDropBlanks,
+    readMaybe,
+    expect,
+    expected,
+
+    -- * Data structures
+    module NationStates.Types,
+
+    ) where
+
+
+import qualified Data.ByteString.Char8 as BC
+import Data.Functor.Compose
+import Data.Foldable (toList)
+import Data.List
+import Data.List.Split
+import Data.Monoid
+import Data.Map (Map)
+import qualified Data.Map as Map
+import Data.Maybe
+import Data.Set (Set)
+import qualified Data.Set as Set
+import Network.HTTP.Client
+import qualified Network.HTTP.Types as HTTP
+import Text.Read
+import Text.XML.Light
+
+import NationStates.Types
+
+
+-- | A request to the NationStates API.
+--
+-- * Construct an @NS@ using 'makeNS' or 'makeNS''.
+-- * Compose @NS@ values using the 'Applicative' interface.
+-- * Execute an @NS@ using 'requestNS'.
+--
+-- This type wraps a query string, along with a function that parses the
+-- response. The funky type machinery keeps these two parts in sync, as
+-- long as you stick to the 'Applicative' interface.
+--
+-- @
+-- type NS a = ('Query', Query -> 'Element' -> a)
+-- @
+type NS = Compose ((,) Query) (Compose ((->) Query) ((->) Element))
+
+
+-- | Construct a request for a single shard.
+--
+-- For example, this code requests the
+-- <https://www.nationstates.net/cgi-bin/api.cgi?nation=testlandia&q=motto "motto">
+-- shard:
+--
+-- @
+-- motto :: NS String
+-- motto = makeNS \"motto\" Nothing \"MOTTO\"
+-- @
+--
+-- For more complex requests (e.g. nested elements), try 'makeNS'' instead.
+makeNS
+    :: String
+        -- ^ Shard name
+    -> String
+        -- ^ XML element name
+    -> NS String
+makeNS shard elemName = makeNS' shard Nothing [] parse
+  where
+    parse _ = strContent . fromMaybe errorMissing . findChild (unqual elemName)
+    errorMissing = error $ "missing <" ++ elemName ++ "> element"
+
+
+-- | Construct a request for a single shard.
+makeNS'
+    :: String
+        -- ^ Shard name
+    -> Maybe Integer
+        -- ^ Shard ID
+    -> [(String, String)]
+        -- ^ List of options
+    -> (Query -> Element -> a)
+        -- ^ Function for parsing the response
+    -> NS a
+makeNS' name maybeId options parse = Compose
+    (Query {
+        queryShards = Map.singleton name (Set.singleton maybeId),
+        queryOptions = Map.fromList options
+    }, Compose parse)
+
+
+-- | Perform a request on the NationStates API.
+requestNS
+    :: Maybe (String, String)
+        -- ^ Request type
+    -> NS a
+        -- ^ Set of shards to request
+    -> Context
+        -- ^ Connection manager
+    -> IO a
+requestNS kindAndName (Compose (q, Compose p)) c
+    = parse . responseBody <$>
+        (contextRateLimit c $ httpLbs req (contextManager c))
+  where
+    parse = p q . fromMaybe (error "invalid response") . parseXMLDoc
+    req = initRequest {
+        queryString
+            = HTTP.renderQuery True (HTTP.toQuery $
+                toList kindAndName ++ [("q", shards), ("v", show apiVersion)])
+            <> BC.pack options,
+        requestHeaders
+            = ("User-Agent", BC.pack $ contextUserAgent c)
+            : requestHeaders initRequest
+        }
+    (shards, options) = queryToUrl q
+
+initRequest :: Request
+Just initRequest = parseUrl "https://www.nationstates.net/cgi-bin/api.cgi"
+
+
+-- | The version of the NationStates API used by this package.
+--
+-- Every request to NationStates includes this number. This means that
+-- if the response format changes, existing code will continue to work
+-- under the old API.
+--
+-- This number should match the current API version, as given by
+-- <https://www.nationstates.net/cgi-bin/api.cgi?a=version>. If not,
+-- please file an issue.
+apiVersion :: Integer
+apiVersion = 7
+
+
+-- | Keeps track of rate limits and TLS connections.
+--
+-- You should create a single 'Context' at the start of your program,
+-- then share it between multiple threads and requests.
+data Context = Context {
+    contextManager :: Manager,
+    contextRateLimit :: forall a. IO a -> IO a,
+    contextUserAgent :: String
+    }
+
+
+-- | Keeps track of the set of shards to request.
+data Query = Query {
+    queryShards :: Map String (Set (Maybe Integer)),
+    queryOptions :: Map String String
+    } deriving Show
+
+instance Monoid Query where
+    mempty = Query mempty mempty
+    mappend a b = Query {
+        queryShards = Map.unionWith Set.union
+            (queryShards a) (queryShards b),
+        queryOptions = Map.unionWithKey mergeOptions
+            (queryOptions a) (queryOptions b)
+        }
+      where
+        mergeOptions key _ _
+            = error $ "conflicting values for option " ++ show key
+
+
+queryToUrl :: Query -> (String, String)
+queryToUrl q = (shards, options)
+  where
+    shards = intercalate "+" [ name ++ foldMap (\i -> "-" ++ show i) maybeId |
+        (name, is) <- Map.toList $ queryShards q,
+        maybeId <- Set.toList is ]
+    options = concat [ ";" ++ k ++ "=" ++ v |
+        (k, v) <- Map.toList $ queryOptions q ]
+
+
+-- | Split a string by a separator, dropping empty substrings.
+--
+-- >>> splitDropBlanks "," "the_vines,motesardo-east_adanzi,yellowapple"
+-- ["the_vines", "montesardo-east_adanzi", "yellowapple"]
+--
+-- >>> splitDropBlanks "," ""
+-- []
+splitDropBlanks :: Eq a => [a] -> [a] -> [[a]]
+splitDropBlanks = split . dropBlanks . dropDelims . onSublist
+
+-- | Parse an input string using the given parser function.
+--
+-- If parsing fails, raise an 'error'.
+--
+-- >>> expect "integer" readMaybe "42" :: Integer
+-- 42
+--
+-- >>> expect "integer" readMaybe "butts" :: Integer
+-- *** Exception: invalid integer: "butts"
+expect :: String -> (String -> Maybe a) -> String -> a
+expect want parse = fromMaybe <$> expected want <*> parse
+
+-- | Raise an 'error'.
+--
+-- >>> expected "integer" "butts"
+-- *** Exception: invalid integer: "butts"
+expected :: String -> String -> a
+expected want s = error $ "invalid " ++ want ++ ": " ++ show s
diff --git a/NationStates/Nation.hs b/NationStates/Nation.hs
new file mode 100644
--- /dev/null
+++ b/NationStates/Nation.hs
@@ -0,0 +1,293 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+-- | The Nation API.
+--
+-- This module should be imported qualified, to prevent name clashes:
+--
+-- @
+-- import NationStates
+-- import qualified NationStates.Nation as Nation
+-- @
+--
+-- In general, this module follows the terminology used in the
+-- <https://www.nationstates.net/pages/api.html#nationapi official documentation>,
+-- except when it clashes with Haskell keywords. For instance, the
+-- @type@ shard has been renamed to 'type_'.
+--
+-- Here's a short example:
+--
+-- @
+-- import NationStates
+-- import qualified NationStates.Nation as Nation
+-- import Text.Printf
+--
+-- main = 'NationStates.withContext' "ExampleBot/2000" $ \\c -> do
+--     (name, motto) <- Nation.'run' "Montesardo-East Adanzi"
+--         ((,) \<$\> Nation.'name' \<*\> Nation.'motto') c
+--     printf "%s has the motto: %s\\n" name motto
+-- @
+
+module NationStates.Nation (
+
+    -- * Running queries
+    Nation(..),
+    run,
+
+    -- * Shards
+    name,
+    fullname,
+    type_,
+    motto,
+    category,
+    wa,
+    endorsements,
+    gavote,
+    scvote,
+    freedom,
+    region,
+    population,
+    tax,
+    animal,
+    animaltrait,
+    currency,
+    flag,
+    banner,
+    banners,
+--    majorindustry,
+--    crime,
+--    sensibilities,
+--    govtpriority,
+--    govt,
+--    govtdesc,
+--    industrydesc,
+--    notable,
+--    admirable,
+--    founded,
+--    firstlogin,
+--    lastlogin,
+--    lastactivity,
+--    influence,
+--    freedomscores,
+--    publicsector,
+--    deaths,
+--    leader,
+--    capital,
+--    religion,
+--    customleader,
+--    customcapital,
+--    customreligion,
+--    rcensus,
+--    wcensus,
+    censusscore,
+    censusscore',
+--    legislation,
+--    happenings,
+--    demonym,
+--    demonym2,
+--    demonym2plural,
+--    factbooks,
+--    factbooklist,
+--    dispatches,
+--    dispatchlist,
+
+    ) where
+
+
+import Data.Maybe
+import qualified Data.Map as Map
+import qualified Data.MultiSet as MultiSet
+import Text.XML.Light
+
+import NationStates.Core
+
+
+-- | A request to the Nation API.
+newtype Nation a = Nation { unNation :: NS a }
+    deriving (Functor, Applicative)
+
+
+-- | Perform a request to the Nation API.
+run
+    :: String
+        -- ^ Nation name
+    -> Nation a
+        -- ^ Requested shards
+    -> Context
+        -- ^ Connection manager
+    -> IO a
+run nation = requestNS (Just ("nation", nation)) . unNation
+
+
+-- | Short name.
+--
+-- > "Testlandia"
+name :: Nation String
+name = Nation $ makeNS "name" "NAME"
+
+-- | Full name, including pre-title.
+--
+-- > "The Republic of Testlandia"
+fullname :: Nation String
+fullname = Nation $ makeNS "fullname" "FULLNAME"
+
+-- | Nation type.
+--
+-- > "Republic"
+type_ :: Nation String
+type_ = Nation $ makeNS "type" "TYPE"
+
+-- | Motto.
+--
+-- > "It's a feature!"
+motto :: Nation String
+motto = Nation $ makeNS "motto" "MOTTO"
+
+-- | Nation category.
+--
+-- > InoffensiveCentristDemocracy
+category :: Nation WACategory
+category = Nation . fmap (expect "category" readWACategory) $
+    makeNS "category" "CATEGORY"
+
+-- | Whether the nation is in the World Assembly.
+--
+-- > True
+wa :: Nation Bool
+wa = Nation . fmap parse $ makeNS "wa" "UNSTATUS"
+  where
+    parse "WA Member" = True
+    parse "Non-member" = False
+    parse s = expected "WA status" s
+
+-- | List of endorsements received.
+--
+-- > ["jlink","translenia","the_vines"]
+endorsements :: Nation [String]
+endorsements = Nation . fmap (splitDropBlanks ",") $
+    makeNS "endorsements" "ENDORSEMENTS"
+
+-- | General assembly vote.
+--
+-- > Just True
+gavote :: Nation (Maybe Bool)
+gavote = Nation . fmap (expect "General Assembly vote" readWAVote) $
+    makeNS "gavote" "GAVOTE"
+
+-- | Security council vote.
+--
+-- > Nothing
+scvote :: Nation (Maybe Bool)
+scvote = Nation . fmap (expect "Security Council vote" readWAVote) $
+    makeNS "scvote" "SCVOTE"
+
+-- | Description of civil rights, economy, and political freedoms.
+--
+-- > ("Excellent","Strong","Very Good")
+freedom :: Nation (String, String, String)
+freedom = Nation $ makeNS' "freedom" Nothing [] parse
+  where
+    parse _ root
+        | Just parent <- findChild (unqual "FREEDOM") root
+        , [c, e, p] <- map strContent $ elChildren parent
+            = (c, e, p)
+        | otherwise
+            = error "could not find freedom descriptors"
+
+-- | Resident region.
+--
+-- > "Testregionia"
+region :: Nation String
+region = Nation $ makeNS "region" "REGION"
+
+-- | Population, in millions.
+--
+-- > 25764
+population :: Nation Integer
+population = Nation . fmap (expect "population" readMaybe) $
+    makeNS "population" "POPULATION"
+
+-- | Income tax, percent.
+--
+-- > 83.6
+tax :: Nation Double
+tax = Nation . fmap (expect "tax" readMaybe) $
+    makeNS "tax" "TAX"
+
+-- | National animal.
+--
+-- > "sea-snake"
+animal :: Nation String
+animal = Nation $ makeNS "animal" "ANIMAL"
+
+-- | A short phrase describing the animal.
+--
+-- > "is also the nation's favorite main course"
+animaltrait :: Nation String
+animaltrait = Nation $ makeNS "animaltrait" "ANIMALTRAIT"
+
+-- | Currency.
+--
+-- > "☆star☆"
+currency :: Nation String
+currency = Nation $ makeNS "currency" "CURRENCY"
+
+-- | Flag URL.
+--
+-- > "http://www.nationstates.net/images/flags/Switzerland.png"
+flag :: Nation String
+flag = Nation $ makeNS "flag" "FLAG"
+
+-- | A suitable banner for this nation.
+--
+-- > "v1"
+banner :: Nation String
+banner = Nation $ makeNS "banner" "BANNER"
+
+-- | A list of suitable banners for this nation.
+--
+-- > ["v1","o4","b14","t23","m3"]
+banners :: Nation [String]
+banners = Nation $ makeNS' "banners" Nothing [] parse
+  where
+    parse _ root
+        | Just parent <- findChild (unqual "BANNERS") root
+            = map strContent $ elChildren parent
+        | otherwise
+            = error "could not find banner codes"
+
+-- | Query today's census.
+--
+-- Returns the current census ID, along with its value.
+--
+-- > (24,6.0)
+censusscore :: Nation (Integer, Double)
+censusscore = Nation $ makeNS' "censusscore" Nothing [] parse
+  where
+    parse q root
+        | Just (i, _) <- MultiSet.minView $ MultiSet.difference response request
+        , Just x <- lookup i censusScores
+            = (i, x)
+        | otherwise
+            = error "could not find census score"
+      where
+        censusScores = extractCensusScores root
+        request = MultiSet.mapMaybe id . MultiSet.fromSet $
+            queryShards q Map.! "censusscore"
+        response = MultiSet.fromList $ map fst censusScores
+
+-- | Query a census by its census ID.
+--
+-- > 94.0
+censusscore' :: Integer -> Nation Double
+censusscore' i = Nation $ makeNS' "censusscore" (Just i) [] parse
+  where
+    parse _ = fromMaybe (error $ "could not find census " ++ show i) .
+        lookup i . extractCensusScores
+
+extractCensusScores :: Element -> [(Integer, Double)]
+extractCensusScores root = catMaybes [
+    (,) <$> maybeId <*> maybeValue |
+    Elem e <- elContent root,
+    elName e == unqual "CENSUSSCORE",
+    let maybeId = readMaybe =<< findAttr (unqual "id") e,
+    let maybeValue = readMaybe $ strContent e ]
diff --git a/NationStates/RateLimit.hs b/NationStates/RateLimit.hs
new file mode 100644
--- /dev/null
+++ b/NationStates/RateLimit.hs
@@ -0,0 +1,46 @@
+-- | Simple rate limiting combinator.
+
+module NationStates.RateLimit (
+    RateLimit(),
+    newRateLimit,
+    rateLimit,
+    ) where
+
+
+import Control.Concurrent
+import Control.Exception
+import System.Clock
+
+
+data RateLimit = RateLimit {
+    rateLock :: !(MVar TimeSpec),
+    rateDelay :: !TimeSpec
+    }
+
+
+-- | Create a new rate limiter with the specified delay.
+--
+-- The rate limiter is thread-safe, and can be shared between threads.
+newRateLimit :: TimeSpec -> IO RateLimit
+newRateLimit delay = do
+    lock <- newMVar $! negate delay
+    return RateLimit {
+        rateLock = lock,
+        rateDelay = delay
+        }
+
+
+-- | Run the given action, pausing as necessary to keep under the rate limit.
+rateLimit :: RateLimit -> IO a -> IO a
+rateLimit RateLimit { rateLock = lock, rateDelay = delay } action =
+    mask $ \restore -> do
+        prev <- takeMVar lock
+        now <- getTime Monotonic
+        threadDelay' $ prev + delay - now
+        result <- restore action
+        putMVar lock =<< getTime Monotonic
+        return result
+
+
+threadDelay' :: TimeSpec -> IO ()
+threadDelay' t = threadDelay . fromInteger $ timeSpecAsNanoSecs t `div` 1000
diff --git a/NationStates/Region.hs b/NationStates/Region.hs
new file mode 100644
--- /dev/null
+++ b/NationStates/Region.hs
@@ -0,0 +1,105 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+-- | The Region API.
+--
+-- This module should be imported qualified, to prevent name clashes:
+--
+-- @
+-- import NationStates
+-- import qualified NationStates.Region as Region
+-- @
+--
+-- In general, this module follows the terminology used in the
+-- <https://www.nationstates.net/pages/api.html#nationapi official documentation>.
+--
+-- Here's a short example:
+--
+-- @
+-- import NationStates
+-- import qualified NationStates.Region as Region
+-- import Text.Printf
+--
+-- main = 'NationStates.withContext' "ExampleBot/2000" $ \\c -> do
+--     (name, numnations, delegate) <- Region.'run' "Pony Lands"
+--         ((,,) \<$\> Region.'name' \<*\> Region.'numnations' \<*\> Region.'delegate') c
+--     printf "%s has %d nations. Its delegate is %s\\n" name numnations delegate
+-- @
+
+module NationStates.Region (
+
+    -- * Running queries
+    Region(..),
+    run,
+
+    -- * Shards
+    name,
+    factbook,
+    numnations,
+    nations,
+    delegate,
+--    delegatevote,
+--    gavote,
+--    scvote,
+--    founder,
+--    power,
+--    flag,
+--    embassies,
+--    tags,
+--    happenings,
+--    messages,
+--    history,
+--    poll,
+
+    ) where
+
+
+import NationStates.Core
+
+
+-- | A request to the Region API.
+newtype Region a = Region { unRegion :: NS a }
+    deriving (Functor, Applicative)
+
+
+-- | Perform a request to the Region API.
+run
+    :: String
+        -- ^ Region name
+    -> Region a
+        -- ^ Requested shards
+    -> Context
+        -- ^ Connection manager
+    -> IO a
+run region = requestNS (Just ("region", region)) . unRegion
+
+
+-- | Region name.
+--
+-- > "Pony Lands"
+name :: Region String
+name = Region $ makeNS "name" "NAME"
+
+-- | Factbook, in BBCode format.
+--
+-- > "[b]We&#39;ve got ponies, therefore your argument is invalid..."
+factbook :: Region String
+factbook = Region $ makeNS "factbook" "FACTBOOK"
+
+-- | Number of nations in the region.
+--
+-- > 112
+numnations :: Region Integer
+numnations = Region . fmap (expect "nation count" readMaybe) $
+    makeNS "numnations" "NUMNATIONS"
+
+-- | List of nations in the region.
+--
+-- > ["urmanian","enatai","unfitting_doors","lykosia","trotterdam"]
+nations :: Region [String]
+nations = Region . fmap (splitDropBlanks ":") $ makeNS "nations" "NATIONS"
+
+-- | Region delegate.
+--
+-- > "princess_luna"
+delegate :: Region String
+delegate = Region $ makeNS "delegate" "DELEGATE"
diff --git a/NationStates/Types.hs b/NationStates/Types.hs
new file mode 100644
--- /dev/null
+++ b/NationStates/Types.hs
@@ -0,0 +1,122 @@
+-- | Data structures used by NationStates.
+
+module NationStates.Types where
+
+
+-- | Nation category.
+--
+-- This datum summarizes a nation's personal, economic, and political
+-- freedoms.
+data WACategory
+    = Anarchy
+    | AuthoritarianDemocracy
+    | BenevolentDictatorship
+    | CapitalistParadise
+    | Capitalizt
+    | CivilRightsLovefest
+    | CompulsoryConsumeristState
+    | ConservativeDemocracy
+    | CorporateBordello
+    | CorporatePoliceState
+    | CorruptDictatorship
+    | DemocraticSocialists
+    | FatherKnowsBestState FatherOrMother
+        -- ^ This category has two variations: \"/Father/ Knows Best
+        -- State\" and \"/Mother/ Knows Best State\".
+    | FreeMarketParadise
+    | InoffensiveCentristDemocracy
+    | IronFistConsumerists
+    | IronFistSocialists
+    | LeftLeaningCollegeState
+    | LeftWingUtopia
+    | LiberalDemocraticSocialists
+    | LibertarianPoliceState
+    | MoralisticDemocracy
+    | NewYorkTimesDemocracy
+    | PsychoticDictatorship
+    | RightWingUtopia
+    | ScandinavianLiberalParadise
+    | TyrannyByMajority
+    deriving (Eq, Ord, Read, Show)
+
+-- | Differentiates between a /Father/ or /Mother/ Knows Best State.
+data FatherOrMother = Father | Mother
+    deriving (Eq, Ord, Read, Show)
+
+
+readWACategory :: String -> Maybe WACategory
+readWACategory s = case s of
+    "Anarchy" -> Just Anarchy
+    "Authoritarian Democracy" -> Just AuthoritarianDemocracy
+    "Benevolent Dictatorship" -> Just BenevolentDictatorship
+    "Capitalist Paradise" -> Just CapitalistParadise
+    "Capitalizt" -> Just Capitalizt
+    "Civil Rights Lovefest" -> Just CivilRightsLovefest
+    "Compulsory Consumerist State" -> Just CompulsoryConsumeristState
+    "Conservative Democracy" -> Just ConservativeDemocracy
+    "Corporate Bordello" -> Just CorporateBordello
+    "Corporate Police State" -> Just CorporatePoliceState
+    "Corrupt Dictatorship" -> Just CorruptDictatorship
+    "Democratic Socialists" -> Just DemocraticSocialists
+    "Father Knows Best State" -> Just $ FatherKnowsBestState Father
+    "Free-Market Paradise" -> Just FreeMarketParadise
+    "Inoffensive Centrist Democracy" -> Just InoffensiveCentristDemocracy
+    "Iron Fist Consumerists" -> Just IronFistConsumerists
+    "Iron Fist Socialists" -> Just IronFistSocialists
+    "Left-Leaning College State" -> Just LeftLeaningCollegeState
+    "Left-wing Utopia" -> Just LeftWingUtopia
+    "Liberal Democratic Socialists" -> Just LiberalDemocraticSocialists
+    "Libertarian Police State" -> Just LibertarianPoliceState
+    "Moralistic Democracy" -> Just MoralisticDemocracy
+    "Mother Knows Best State" -> Just $ FatherKnowsBestState Mother
+    "New York Times Democracy" -> Just NewYorkTimesDemocracy
+    "Psychotic Dictatorship" -> Just PsychoticDictatorship
+    "Right-wing Utopia" -> Just RightWingUtopia
+    "Scandinavian Liberal Paradise" -> Just ScandinavianLiberalParadise
+    "Tyranny by Majority" -> Just TyrannyByMajority
+    _ -> Nothing
+
+showWACategory :: WACategory -> String
+showWACategory c = case c of
+    Anarchy -> "Anarchy"
+    AuthoritarianDemocracy -> "Authoritarian Democracy"
+    BenevolentDictatorship -> "Benevolent Dictatorship"
+    CapitalistParadise -> "Capitalist Paradise"
+    Capitalizt -> "Capitalizt"
+    CivilRightsLovefest -> "Civil Rights Lovefest"
+    CompulsoryConsumeristState -> "Compulsory Consumerist State"
+    ConservativeDemocracy -> "Conservative Democracy"
+    CorporateBordello -> "Corporate Bordello"
+    CorporatePoliceState -> "Corporate Police State"
+    CorruptDictatorship -> "Corrupt Dictatorship"
+    DemocraticSocialists -> "Democratic Socialists"
+    FatherKnowsBestState Father -> "Father Knows Best State"
+    FatherKnowsBestState Mother -> "Mother Knows Best State"
+    FreeMarketParadise -> "Free-Market Paradise"
+    InoffensiveCentristDemocracy -> "Inoffensive Centrist Democracy"
+    IronFistConsumerists -> "Iron Fist Consumerists"
+    IronFistSocialists -> "Iron Fist Socialists"
+    LeftLeaningCollegeState -> "Left-Leaning College State"
+    LeftWingUtopia -> "Left-wing Utopia"
+    LiberalDemocraticSocialists -> "Liberal Democratic Socialists"
+    LibertarianPoliceState -> "Libertarian Police State"
+    MoralisticDemocracy -> "Moralistic Democracy"
+    NewYorkTimesDemocracy -> "New York Times Democracy"
+    PsychoticDictatorship -> "Psychotic Dictatorship"
+    RightWingUtopia -> "Right-wing Utopia"
+    ScandinavianLiberalParadise -> "Scandinavian Liberal Paradise"
+    TyrannyByMajority -> "Tyranny by Majority"
+
+
+readWAVote :: String -> Maybe (Maybe Bool)
+readWAVote s = case s of
+    "UNDECIDED" -> Just Nothing
+    "FOR" -> Just $ Just True
+    "AGAINST" -> Just $ Just False
+    _ -> Nothing
+
+showWAVote :: Maybe Bool -> String
+showWAVote v = case v of
+    Nothing -> "UNDECIDED"
+    Just True -> "FOR"
+    Just False -> "AGAINST"
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,30 @@
+# NationStates for Haskell
+
+[NationStates] is an online government simulation game, created by Max Barry. The site generates a wealth of data, some of which can be accessed through its [official API].
+
+This library lets you query this API using the Haskell programming language.
+
+[NationStates]: https://nationstates.net
+[official API]: https://www.nationstates.net/pages/api.html
+
+
+## Dependencies
+
+* GHC 7.10
+
+    + GHC 7.8 support is planned – patches welcome!
+
+
+## Example
+
+```haskell
+import NationStates
+import qualified NationStates.Nation as Nation
+import Text.Printf
+
+main = withContext "ExampleBot/2000" $ \c -> do
+    (name, motto) <- Nation.run "Montesardo-East Adanzi" shards c
+    printf "%s has the motto: %s\n" name motto
+  where
+    shards = (,) <$> Nation.name <*> Nation.motto
+```
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/nationstates.cabal b/nationstates.cabal
new file mode 100644
--- /dev/null
+++ b/nationstates.cabal
@@ -0,0 +1,43 @@
+name: nationstates
+version: 0.1.0
+synopsis: NationStates API client
+description:
+    NationStates API client
+
+homepage: https://github.com/lfairy/nationstates
+author: Chris Wong
+maintainer: lambda.fairy@gmail.com
+copyright: 2015 Chris Wong
+license: Apache-2.0
+category: Network, Web
+
+build-type: Simple
+cabal-version: >= 1.10
+extra-source-files: README.md
+
+library
+    exposed-modules:
+        NationStates
+        NationStates.Core
+        NationStates.RateLimit
+        NationStates.Nation
+        NationStates.Region
+        NationStates.Types
+    build-depends:
+        base >= 4.8 && < 5,
+        bytestring,
+        clock,
+        containers,
+        http-client,
+        http-client-tls,
+        http-types,
+        multiset,
+        split,
+        transformers,
+        xml
+    default-language: Haskell2010
+    ghc-options: -Wall
+
+source-repository head
+    type: git
+    location: https://github.com/lfairy/nationstates.git
