diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,19 @@
 Changelog
 =========
 
+Version 0.2.9.0
+---------------
+
+*December 11, 2023*
+
+<https://github.com/mstksg/advent-of-code-api/releases/tag/v0.2.9.0>
+
+*   All API requests now require providing a structured user agent data type,
+    to follow
+    <https://www.reddit.com/r/adventofcode/comments/z9dhtd/please_include_your_contact_info_in_the_useragent/>.
+    This is not enforced in the raw servant API, but is enforced in the
+    "regulated" `Advent` module.
+
 Version 0.2.8.5
 ---------------
 
diff --git a/advent-of-code-api.cabal b/advent-of-code-api.cabal
--- a/advent-of-code-api.cabal
+++ b/advent-of-code-api.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           advent-of-code-api
-version:        0.2.8.5
+version:        0.2.9.0
 synopsis:       Advent of Code REST API bindings and servant API
 description:    Haskell bindings for Advent of Code REST API and a servant API.  Please use
                 responsibly! See README.md or "Advent" module for an introduction and
diff --git a/src/Advent.hs b/src/Advent.hs
--- a/src/Advent.hs
+++ b/src/Advent.hs
@@ -293,6 +293,9 @@
       _aSessionKey :: String
       -- | Year of challenge
     , _aYear       :: Integer
+      -- | Structured user agent to use.  See
+      -- <https://www.reddit.com/r/adventofcode/comments/z9dhtd/please_include_your_contact_info_in_the_useragent/>
+    , _aUserAgent  :: AoCUserAgent
       -- | Cache directory.  If 'Nothing' is given, one will be allocated
       -- using 'getTemporaryDirectory'.
     , _aCache      :: Maybe FilePath
@@ -305,17 +308,20 @@
     }
   deriving (Show, Typeable, Generic)
 
--- | Sensible defaults for 'AoCOpts' for a given year and session key.
+-- | Sensible defaults for 'AoCOpts' for a given user agent, year and session
+-- key.
 --
 -- Use system temporary directory as cache, and throttle requests to one
 -- request per three seconds.
 defaultAoCOpts
-    :: Integer
+    :: AoCUserAgent
+    -> Integer
     -> String
     -> AoCOpts
-defaultAoCOpts y s = AoCOpts
+defaultAoCOpts aua y s = AoCOpts
     { _aSessionKey = s
     , _aYear       = y
+    , _aUserAgent  = aua
     , _aCache      = Nothing
     , _aForce      = False
     , _aThrottle   = 3000000
@@ -326,19 +332,19 @@
 aocBase = BaseUrl Https "adventofcode.com" 443 ""
 
 -- | 'ClientM' request for a given 'AoC' API call.
-aocReq :: Integer -> AoC a -> ClientM a
-aocReq yr = \case
-    AoCPrompt i       -> let r :<|> _        = adventAPIPuzzleClient yr i in r
-    AoCInput  i       -> let _ :<|> r :<|> _ = adventAPIPuzzleClient yr i in r
-    AoCSubmit i p ans -> let _ :<|> _ :<|> r = adventAPIPuzzleClient yr i
+aocReq :: Maybe AoCUserAgent -> Integer -> AoC a -> ClientM a
+aocReq aua yr = \case
+    AoCPrompt i       -> let r :<|> _        = adventAPIPuzzleClient aua yr i in r
+    AoCInput  i       -> let _ :<|> r :<|> _ = adventAPIPuzzleClient aua yr i in r
+    AoCSubmit i p ans -> let _ :<|> _ :<|> r = adventAPIPuzzleClient aua yr i
                          in  r (SubmitInfo p ans) <&> \(x :<|> y) -> (x, y)
-    AoCLeaderboard c  -> let _ :<|> _ :<|> _ :<|> _ :<|> r = adventAPIClient yr
+    AoCLeaderboard c  -> let _ :<|> _ :<|> _ :<|> _ :<|> r = adventAPIClient aua yr
                          in  r (PublicCode c)
-    AoCDailyLeaderboard d -> let _ :<|> _ :<|> _ :<|> r :<|> _ = adventAPIClient yr
+    AoCDailyLeaderboard d -> let _ :<|> _ :<|> _ :<|> r :<|> _ = adventAPIClient aua yr
                              in  r d
-    AoCGlobalLeaderboard  -> let _ :<|> _ :<|> r :<|> _ = adventAPIClient yr
+    AoCGlobalLeaderboard  -> let _ :<|> _ :<|> r :<|> _ = adventAPIClient aua yr
                              in  r
-    AoCNextDayTime        -> let r :<|> _ = adventAPIClient yr
+    AoCNextDayTime        -> let r :<|> _ = adventAPIClient aua yr
                              in  r
 
 
@@ -397,7 +403,7 @@
 
       mtr <- liftIO
            . throttling aocThrottler (max 1000000 _aThrottle)
-           $ runClientM (aocReq _aYear a) =<< aocClientEnv _aSessionKey
+           $ runClientM (aocReq (Just _aUserAgent) _aYear a) =<< aocClientEnv _aSessionKey
       mcr <- maybe (throwError AoCThrottleError) pure mtr
       either (throwError . AoCClientError) pure mcr
 
diff --git a/src/Advent/API.hs b/src/Advent/API.hs
--- a/src/Advent/API.hs
+++ b/src/Advent/API.hs
@@ -35,6 +35,7 @@
 module Advent.API (
   -- * Servant API
     AdventAPI
+  , AoCUserAgent(..)
   , adventAPI
   , adventAPIClient
   , adventAPIPuzzleClient
@@ -228,11 +229,23 @@
           where
             t' = "var " <> t <> " = "
 
+-- | A structured user agent, based on
+-- <https://www.reddit.com/r/adventofcode/comments/z9dhtd/please_include_your_contact_info_in_the_useragent/>
+data AoCUserAgent = AoCUserAgent
+    { _auaRepo :: Text      -- ^ repository where your code is hosted
+    , _auaEmail :: Text     -- ^ email address or contact
+    }
+  deriving (Show)
+
+instance ToHttpApiData AoCUserAgent where
+  toQueryParam AoCUserAgent{..} = _auaRepo <> " " <> _auaEmail
+
 -- | REST API of Advent of Code.
 --
 -- Note that most of these requests assume a "session=" cookie.
 type AdventAPI =
-      Capture "year" Integer
+      Header "User-Agent" AoCUserAgent
+   :> Capture "year" Integer
    :> (Get '[Scripts] NextDayTime
   :<|> "day" :> Capture "day" Day
              :> (Get '[Articles] (Map Part Text)
@@ -257,7 +270,8 @@
 
 -- | 'ClientM' requests based on 'AdventAPI', generated by servant.
 adventAPIClient
-    :: Integer
+    :: Maybe AoCUserAgent
+    -> Integer
     -> ClientM NextDayTime
   :<|> (Day -> ClientM (Map Part Text) :<|> ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes)) )
   :<|> ClientM GlobalLeaderboard
@@ -268,12 +282,13 @@
 -- | A subset of 'adventAPIClient' for only puzzle-related API routes, not
 -- leaderboard ones.
 adventAPIPuzzleClient
-    :: Integer
+    :: Maybe AoCUserAgent
+    -> Integer
     -> Day
     -> ClientM (Map Part Text) :<|> ClientM Text :<|> (SubmitInfo -> ClientM (Text :<|> SubmitRes))
-adventAPIPuzzleClient y = pis
+adventAPIPuzzleClient aua y = pis
   where
-    _ :<|> pis :<|> _ = adventAPIClient y
+    _ :<|> pis :<|> _ = adventAPIClient aua y
 
 userNameNaked :: [TagTree Text] -> Maybe Text
 userNameNaked = (listToMaybe .) . mapMaybe $ \x -> do
