diff --git a/capy/src/main/java/com/jocmp/capy/Account.kt b/capy/src/main/java/com/jocmp/capy/Account.kt
index faa7c15b..6f2650bd 100644
--- a/capy/src/main/java/com/jocmp/capy/Account.kt
+++ b/capy/src/main/java/com/jocmp/capy/Account.kt
@@ -6,10 +6,9 @@ import com.jocmp.capy.accounts.LocalAccountDelegate
 import com.jocmp.capy.accounts.LocalOkHttpClient
 import com.jocmp.capy.accounts.Source
 import com.jocmp.capy.accounts.asOPML
+import com.jocmp.capy.accounts.reader.buildFreshRSSDelegate
 import com.jocmp.capy.accounts.feedbin.FeedbinAccountDelegate
 import com.jocmp.capy.accounts.feedbin.FeedbinOkHttpClient
-import com.jocmp.capy.accounts.reader.ReaderAccountDelegate
-import com.jocmp.capy.accounts.reader.ReaderOkHttpClient
 import com.jocmp.capy.articles.UnreadSortOrder
 import com.jocmp.capy.common.TimeHelpers.nowUTC
 import com.jocmp.capy.common.sortedByTitle
@@ -20,7 +19,6 @@ import com.jocmp.capy.opml.OPMLImporter
 import com.jocmp.capy.persistence.ArticleRecords
 import com.jocmp.capy.persistence.FeedRecords
 import com.jocmp.feedbinclient.Feedbin
-import com.jocmp.readerclient.GoogleReader
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
@@ -258,23 +256,6 @@ data class Account(
 private fun Feedbin.Companion.forAccount(path: URI, preferences: AccountPreferences) =
     create(client = FeedbinOkHttpClient.forAccount(path, preferences))
 
-private fun buildFreshRSSDelegate(
-    database: Database,
-    path: URI,
-    preferences: AccountPreferences
-): AccountDelegate {
-    val httpClient = ReaderOkHttpClient.forAccount(path, preferences)
-
-    return ReaderAccountDelegate(
-        database = database,
-        httpClient = httpClient,
-        googleReader = GoogleReader.create(
-            client = httpClient,
-            baseURL = preferences.url.get()
-        )
-    )
-}
-
 private fun AutoDelete.cutoffDate(): ZonedDateTime? {
     val now = nowUTC()
 
diff --git a/feedbinclient/src/main/java/com/jocmp/feedbinclient/BasicAuthInterceptor.kt b/capy/src/main/java/com/jocmp/capy/accounts/BasicAuthInterceptor.kt
similarity index 82%
rename from feedbinclient/src/main/java/com/jocmp/feedbinclient/BasicAuthInterceptor.kt
rename to capy/src/main/java/com/jocmp/capy/accounts/BasicAuthInterceptor.kt
index 5e6c06f0..c3424ef9 100644
--- a/feedbinclient/src/main/java/com/jocmp/feedbinclient/BasicAuthInterceptor.kt
+++ b/capy/src/main/java/com/jocmp/capy/accounts/BasicAuthInterceptor.kt
@@ -1,4 +1,4 @@
-package com.jocmp.feedbinclient
+package com.jocmp.capy.accounts
 
 import okhttp3.Interceptor
 
@@ -6,7 +6,7 @@ class BasicAuthInterceptor(private val credentials: () -> String) : Interceptor
     override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
         val request = chain.request()
 
-        if (request.headers("Authorization").isNullOrEmpty()) {
+        if (request.headers("Authorization").isEmpty()) {
             val authenticatedRequest =
                 request.newBuilder().header("Authorization", credentials()).build()
             return chain.proceed(authenticatedRequest)
diff --git a/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt b/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt
index 02cf4529..4970039a 100644
--- a/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt
+++ b/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegate.kt
@@ -283,7 +283,7 @@ internal class FeedbinAccountDelegate(
         coroutineScope {
             ids.chunked(MAX_ENTRY_LIMIT).map { chunkedIDs ->
                 launch {
-                    fetchPaginatedEntries(ids = chunkedIDs)
+                    fetchPaginatedEntries(ids = chunkedIDs.map { it.toLong() })
                 }
             }
         }
diff --git a/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinOkHttpClient.kt b/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinOkHttpClient.kt
index 440af09d..68aa3e36 100644
--- a/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinOkHttpClient.kt
+++ b/capy/src/main/java/com/jocmp/capy/accounts/feedbin/FeedbinOkHttpClient.kt
@@ -2,7 +2,7 @@ package com.jocmp.capy.accounts.feedbin
 
 import com.jocmp.capy.AccountPreferences
 import com.jocmp.capy.accounts.httpClientBuilder
-import com.jocmp.feedbinclient.BasicAuthInterceptor
+import com.jocmp.capy.accounts.BasicAuthInterceptor
 import okhttp3.Credentials
 import okhttp3.OkHttpClient
 import java.net.URI
diff --git a/capy/src/main/java/com/jocmp/capy/accounts/reader/BuildFreshRSSDelegate.kt b/capy/src/main/java/com/jocmp/capy/accounts/reader/BuildFreshRSSDelegate.kt
new file mode 100644
index 00000000..8a7e9dff
--- /dev/null
+++ b/capy/src/main/java/com/jocmp/capy/accounts/reader/BuildFreshRSSDelegate.kt
@@ -0,0 +1,25 @@
+package com.jocmp.capy.accounts.reader
+
+import com.jocmp.capy.AccountDelegate
+import com.jocmp.capy.AccountPreferences
+import com.jocmp.capy.accounts.httpClientBuilder
+import com.jocmp.capy.db.Database
+import com.jocmp.readerclient.GoogleReader
+import java.net.URI
+
+internal fun buildFreshRSSDelegate(
+    database: Database,
+    path: URI,
+    preferences: AccountPreferences
+): AccountDelegate {
+    val httpClient = ReaderOkHttpClient.forAccount(path, preferences)
+
+    return ReaderAccountDelegate(
+        database = database,
+        httpClient = httpClientBuilder(cachePath = path).build(),
+        googleReader = GoogleReader.create(
+            client = httpClient,
+            baseURL = preferences.url.get()
+        )
+    )
+}
diff --git a/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt b/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt
index 8f54360b..6c3460f7 100644
--- a/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt
+++ b/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegate.kt
@@ -13,23 +13,26 @@ import com.jocmp.capy.db.Database
 import com.jocmp.capy.persistence.ArticleRecords
 import com.jocmp.readerclient.Category
 import com.jocmp.readerclient.GoogleReader
+import com.jocmp.readerclient.GoogleReader.Companion.BAD_TOKEN_HEADER_KEY
 import com.jocmp.readerclient.Item
+import com.jocmp.readerclient.ItemRef
 import com.jocmp.readerclient.Stream
 import com.jocmp.readerclient.Subscription
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
 import okhttp3.OkHttpClient
 import org.jsoup.Jsoup
+import retrofit2.Response
 import java.io.IOException
 import java.time.ZonedDateTime
+import java.util.concurrent.atomic.AtomicReference
 
-/**
- * Save Auth Token for later use
- * self.credentials = Credentials(type: .readerAPIKey, username: credentials.username, secret: authString)
- */
 internal class ReaderAccountDelegate(
     private val database: Database,
-    private val httpClient: OkHttpClient,
     private val googleReader: GoogleReader,
+    httpClient: OkHttpClient = OkHttpClient(),
 ) : AccountDelegate {
+    private var postToken = AtomicReference<String?>(null)
     private val articleContent = ArticleContent(httpClient)
     private val articleRecords = ArticleRecords(database)
 
@@ -120,7 +123,7 @@ internal class ReaderAccountDelegate(
             title = subscription.title,
             feed_url = subscription.url,
             site_url = subscription.htmlUrl,
-            favicon_url = subscription.iconUrl
+            favicon_url = subscription.iconUrl.ifBlank { null }
         )
     }
 
@@ -139,8 +142,8 @@ internal class ReaderAccountDelegate(
     private suspend fun refreshArticles(
         since: Long = articleRecords.maxUpdatedAt().toEpochSecond()
     ) {
-        refreshUnreadItems()
         refreshStarredItems()
+        refreshUnreadItems()
         refreshAllArticles(since = since)
         fetchMissingArticles()
     }
@@ -162,8 +165,25 @@ internal class ReaderAccountDelegate(
         }
     }
 
+    private suspend fun fetchMissingArticles() {
+        val ids = articleRecords.findMissingArticles()
+
+        coroutineScope {
+            ids.chunked(MAX_PAGINATED_ITEM_LIMIT).map { chunkedIDs ->
+                launch {
+                    val response = withPostToken {
+                        googleReader.streamItemsContents(
+                            postToken = postToken.get(),
+                            ids = chunkedIDs
+                        )
+                    }
+
+                    val result = response.body() ?: return@launch
 
-    private fun fetchMissingArticles() {
+                    saveItems(result.items)
+                }
+            }
+        }
     }
 
     private suspend fun refreshAllArticles(since: Long) {
@@ -178,15 +198,21 @@ internal class ReaderAccountDelegate(
         stream: Stream,
         continuation: String? = null,
     ) {
-        val response = googleReader.streamContents(
+        val response = googleReader.streamItemsIDs(
             streamID = stream.id,
             since = since,
             continuation = continuation,
+            excludedStreamID = Stream.READ.id,
+            count = MAX_PAGINATED_ITEM_LIMIT,
         )
 
         val result = response.body() ?: return
 
-        saveItems(result.items)
+        coroutineScope {
+            launch {
+                fetchItemContents(result.itemRefs)
+            }
+        }
 
         val nextContinuation = result.continuation ?: return
 
@@ -197,6 +223,19 @@ internal class ReaderAccountDelegate(
         )
     }
 
+    private suspend fun fetchItemContents(items: List<ItemRef>) {
+        val response = withPostToken {
+            googleReader.streamItemsContents(
+                postToken = postToken.get(),
+                ids = items.map { it.hexID }
+            )
+        }
+
+        val result = response.body() ?: return
+
+        saveItems(result.items)
+    }
+
     private fun saveItems(items: List<Item>) {
         database.transactionWithErrorHandling {
             items.forEach { item ->
@@ -207,7 +246,7 @@ internal class ReaderAccountDelegate(
                     feed_id = item.origin.streamId,
                     title = item.title,
                     author = item.author,
-                    content_html = item.summary.content,
+                    content_html = item.content?.content ?: item.summary.content,
                     extracted_content_url = null,
                     summary = Jsoup.parse(item.summary.content).text(),
                     url = item.canonical.firstOrNull()?.href,
@@ -224,7 +263,34 @@ internal class ReaderAccountDelegate(
         }
     }
 
+    private suspend fun <T> withPostToken(handler: suspend () -> Response<T>): Response<T> {
+        val response = handler()
+
+        val isBadToken = response
+            .headers()
+            .get(BAD_TOKEN_HEADER_KEY)
+            .orEmpty()
+            .toBoolean()
+
+        if (!isBadToken) {
+            return response
+        }
+
+        try {
+            postToken.set(googleReader.token().body())
+
+            return handler()
+        } catch (exception: IOException) {
+            return response
+        }
+    }
+
     private fun taggingID(subscription: Subscription, category: Category): String {
         return "${subscription.id}:${category.id}"
     }
+
+
+    companion object {
+        const val MAX_PAGINATED_ITEM_LIMIT = 100
+    }
 }
diff --git a/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderOkHttpClient.kt b/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderOkHttpClient.kt
index a2cbcbf7..30e55f55 100644
--- a/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderOkHttpClient.kt
+++ b/capy/src/main/java/com/jocmp/capy/accounts/reader/ReaderOkHttpClient.kt
@@ -1,8 +1,8 @@
 package com.jocmp.capy.accounts.reader
 
 import com.jocmp.capy.AccountPreferences
+import com.jocmp.capy.accounts.BasicAuthInterceptor
 import com.jocmp.capy.accounts.httpClientBuilder
-import com.jocmp.feedbinclient.BasicAuthInterceptor
 import okhttp3.OkHttpClient
 import java.net.URI
 
diff --git a/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt b/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt
index d2f84b4d..a44f9e3e 100644
--- a/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt
+++ b/capy/src/main/java/com/jocmp/capy/persistence/ArticleRecords.kt
@@ -33,12 +33,11 @@ internal class ArticleRecords internal constructor(
         ).executeAsOneOrNull()
     }
 
-    fun findMissingArticles(): List<Long> {
+    fun findMissingArticles(): List<String> {
         return database
             .articlesQueries
             .findMissingArticles()
             .executeAsList()
-            .map { it.toLong() }
     }
 
     internal suspend fun notifications(since: ZonedDateTime): List<ArticleNotification> {
diff --git a/capy/src/test/java/com/jocmp/capy/accounts/FeedbinAccountDelegateTest.kt b/capy/src/test/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegateTest.kt
similarity index 90%
rename from capy/src/test/java/com/jocmp/capy/accounts/FeedbinAccountDelegateTest.kt
rename to capy/src/test/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegateTest.kt
index f635c57c..f6be149a 100644
--- a/capy/src/test/java/com/jocmp/capy/accounts/FeedbinAccountDelegateTest.kt
+++ b/capy/src/test/java/com/jocmp/capy/accounts/feedbin/FeedbinAccountDelegateTest.kt
@@ -1,16 +1,16 @@
-package com.jocmp.capy.accounts
+package com.jocmp.capy.accounts.feedbin
 
+import com.jocmp.capy.AccountDelegate
 import com.jocmp.capy.ArticleStatus
 import com.jocmp.capy.InMemoryDatabaseProvider
-import com.jocmp.capy.accounts.feedbin.FeedbinAccountDelegate
+import com.jocmp.capy.accounts.AddFeedResult
+import com.jocmp.capy.accounts.SubscriptionChoice
 import com.jocmp.capy.articles.UnreadSortOrder
 import com.jocmp.capy.db.Database
 import com.jocmp.capy.fixtures.FeedFixture
 import com.jocmp.capy.persistence.ArticleRecords
 import com.jocmp.feedbinclient.CreateSubscriptionRequest
 import com.jocmp.feedbinclient.Entry
-import com.jocmp.feedbinclient.Entry.Images
-import com.jocmp.feedbinclient.Entry.Images.SizeOne
 import com.jocmp.feedbinclient.Feedbin
 import com.jocmp.feedbinclient.StarredEntriesRequest
 import com.jocmp.feedbinclient.Subscription
@@ -27,10 +27,10 @@ import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.Protocol
 import okhttp3.Request
 import okhttp3.ResponseBody.Companion.toResponseBody
-import org.junit.Before
 import org.junit.Test
 import retrofit2.Response
 import java.net.SocketTimeoutException
+import kotlin.test.BeforeTest
 import kotlin.test.assertEquals
 import kotlin.test.assertFalse
 import kotlin.test.assertTrue
@@ -40,6 +40,7 @@ class FeedbinAccountDelegateTest {
     private lateinit var database: Database
     private lateinit var feedbin: Feedbin
     private lateinit var feedFixture: FeedFixture
+    private lateinit var delegate: AccountDelegate
 
     private val subscriptions = listOf(
         Subscription(
@@ -80,26 +81,27 @@ class FeedbinAccountDelegateTest {
             created_at = "2024-02-23T17:47:45.708056Z",
             extracted_content_url = "https://extract.feedbin.com/parser/feedbin/fa2d8d34c403421a766dbec46c58738c36ff359e?base64_url=aHR0cHM6Ly9hcnN0ZWNobmljYS5jb20vP3A9MjAwNTUyNg==",
             author = "Scharon Harding",
-            images = Images(
+            images = Entry.Images(
                 original_url = "https://cdn.arstechnica.net/wp-content/uploads/2024/02/GettyImages-2023785321-800x534.jpg",
-                size_1 = SizeOne(
+                size_1 = Entry.Images.SizeOne(
                     cdn_url = "https://cdn.arstechnica.net/wp-content/uploads/2024/02/GettyImages-2023785321-800x534.jpg"
                 ),
             ),
         )
     )
 
-    @Before
+    @BeforeTest
     fun setup() {
         database = InMemoryDatabaseProvider.build(accountID)
         feedFixture = FeedFixture(database)
-        feedbin = mockk<Feedbin>()
+        feedbin = mockk()
+        delegate = FeedbinAccountDelegate(database, feedbin)
 
         coEvery { feedbin.icons() }.returns(Response.success(listOf()))
     }
 
     @Test
-    fun refreshAll_updatesEntries() = runTest {
+    fun refresh_updatesEntries() = runTest {
         coEvery { feedbin.subscriptions() }.returns(Response.success(subscriptions))
         coEvery { feedbin.unreadEntries() }.returns(Response.success(entries.map { it.id }))
         coEvery { feedbin.starredEntries() }.returns(Response.success(emptyList()))
@@ -113,8 +115,6 @@ class FeedbinAccountDelegateTest {
             )
         }.returns(Response.success(entries))
 
-        val delegate = FeedbinAccountDelegate(database, feedbin)
-
         delegate.refresh()
 
         val articles = database
@@ -141,19 +141,17 @@ class FeedbinAccountDelegateTest {
     }
 
     @Test
-    fun refreshAll_IOException() = runTest {
+    fun refresh_IOException() = runTest {
         val networkError = SocketTimeoutException("Sorry networked charlie")
         coEvery { feedbin.subscriptions() }.throws(networkError)
 
-        val delegate = FeedbinAccountDelegate(database, feedbin)
-
         val result = delegate.refresh()
 
         assertEquals(result, Result.failure(networkError))
     }
 
     @Test
-    fun refreshAll_findsMissingArticles() = runTest {
+    fun refresh_findsMissingArticles() = runTest {
         val unreadEntry = Entry(
             id = 1,
             feed_id = 2,
@@ -165,9 +163,9 @@ class FeedbinAccountDelegateTest {
             created_at = "2024-02-23T17:47:45.708056Z",
             extracted_content_url = "https://extract.feedbin.com/parser/feedbin/fa2d8d34c403421a766dbec46c58738c36ff359e?base64_url=aHR0cHM6Ly9hcnN0ZWNobmljYS5jb20vP3A9MjAwNTUyNg==",
             author = "Scharon Harding",
-            images = Images(
+            images = Entry.Images(
                 original_url = "https://cdn.arstechnica.net/wp-content/uploads/2024/02/GettyImages-2023785321-800x534.jpg",
-                size_1 = SizeOne(
+                size_1 = Entry.Images.SizeOne(
                     cdn_url = "https://cdn.arstechnica.net/wp-content/uploads/2024/02/GettyImages-2023785321-800x534.jpg"
                 ),
             ),
@@ -184,9 +182,9 @@ class FeedbinAccountDelegateTest {
             created_at = "2024-08-243T17:47:45.708056Z",
             extracted_content_url = "https://extract.feedbin.com/parser/feedbin/fa2d8d34c403421a766dbec46c58738c36ff359e?base64_url=aHR0cHM6Ly9hcnN0ZWNobmljYS5jb20vP3A9MjAwNTUyNg==",
             author = "Jay Peters",
-            images = Images(
+            images = Entry.Images(
                 original_url = "https://cdn.arstechnica.net/wp-content/uploads/2024/02/GettyImages-2023785321-800x534.jpg",
-                size_1 = SizeOne(
+                size_1 = Entry.Images.SizeOne(
                     cdn_url = "https://cdn.arstechnica.net/wp-content/uploads/2024/02/GettyImages-2023785321-800x534.jpg"
                 ),
             ),
@@ -215,8 +213,6 @@ class FeedbinAccountDelegateTest {
             )
         }.returns(Response.success(emptyList()))
 
-        val delegate = FeedbinAccountDelegate(database, feedbin)
-
         delegate.refresh()
 
         val starredArticles = ArticleRecords(database)
@@ -244,8 +240,6 @@ class FeedbinAccountDelegateTest {
             null
         )
 
-        val delegate = FeedbinAccountDelegate(database, feedbin)
-
         delegate.markRead(listOf(id.toString()))
 
         coVerify { feedbin.deleteUnreadEntries(body = UnreadEntriesRequest(listOf(id))) }
@@ -259,8 +253,6 @@ class FeedbinAccountDelegateTest {
             listOf(id)
         )
 
-        val delegate = FeedbinAccountDelegate(database, feedbin)
-
         delegate.markUnread(listOf(id.toString()))
 
         coVerify { feedbin.createUnreadEntries(body = UnreadEntriesRequest(listOf(id))) }
@@ -274,8 +266,6 @@ class FeedbinAccountDelegateTest {
             listOf(id)
         )
 
-        val delegate = FeedbinAccountDelegate(database, feedbin)
-
         delegate.addStar(listOf(id.toString()))
 
         coVerify { feedbin.createStarredEntries(body = StarredEntriesRequest(listOf(id))) }
@@ -289,8 +279,6 @@ class FeedbinAccountDelegateTest {
             null
         )
 
-        val delegate = FeedbinAccountDelegate(database, feedbin)
-
         delegate.removeStar(listOf(id.toString()))
 
         coVerify { feedbin.deleteStarredEntries(body = StarredEntriesRequest(listOf(id))) }
@@ -298,7 +286,6 @@ class FeedbinAccountDelegateTest {
 
     @Test
     fun addFeed() = runTest {
-        val delegate = FeedbinAccountDelegate(database, feedbin)
         val url = "wheresyoured.at"
         val successResponse = Response.success<Subscription>(
             Subscription(
@@ -326,7 +313,7 @@ class FeedbinAccountDelegateTest {
             )
         }.returns(Response.success(emptyList()))
 
-        val result = delegate.addFeed(url = url) as AddFeedResult.Success
+        val result = delegate.addFeed(url = url, folderTitles = emptyList(), title = "") as AddFeedResult.Success
         val feed = result.feed
 
         assertEquals(
@@ -337,7 +324,6 @@ class FeedbinAccountDelegateTest {
 
     @Test
     fun addFeed_multipleChoice() = runTest {
-        val delegate = FeedbinAccountDelegate(database, feedbin)
         val url = "9to5google.com"
         val choices = listOf(
             SubscriptionChoice(
@@ -373,7 +359,7 @@ class FeedbinAccountDelegateTest {
             feedbin.createSubscription(body = CreateSubscriptionRequest(feed_url = url))
         } returns multipleChoiceResponse
 
-        val result = delegate.addFeed(url = url)
+        val result = delegate.addFeed(url = url, folderTitles = emptyList(), title = "")
 
         val actualTitles =
             (result as AddFeedResult.MultipleChoices).choices.map { it.title }
@@ -383,7 +369,6 @@ class FeedbinAccountDelegateTest {
 
     @Test
     fun addFeed_Failure() = runTest {
-        val delegate = FeedbinAccountDelegate(database, feedbin)
         val url = "example.com"
 
         val responseBody = """
@@ -398,7 +383,7 @@ class FeedbinAccountDelegateTest {
             feedbin.createSubscription(body = CreateSubscriptionRequest(feed_url = url))
         } returns Response.error(404, responseBody)
 
-        val result = delegate.addFeed(url = url)
+        val result = delegate.addFeed(url = url, folderTitles = emptyList(), title = "")
 
         assertTrue(result is AddFeedResult.Failure)
     }
@@ -435,6 +420,3 @@ class FeedbinAccountDelegateTest {
         assertEquals(expected = feedTitle, actual = updated.title)
     }
 }
-
-private suspend fun FeedbinAccountDelegate.addFeed(url: String) =
-    addFeed(url = url, null, null)
diff --git a/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt b/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt
new file mode 100644
index 00000000..50b62177
--- /dev/null
+++ b/capy/src/test/java/com/jocmp/capy/accounts/reader/ReaderAccountDelegateTest.kt
@@ -0,0 +1,272 @@
+package com.jocmp.capy.accounts.reader
+
+import com.jocmp.capy.AccountDelegate
+import com.jocmp.capy.ArticleStatus
+import com.jocmp.capy.InMemoryDatabaseProvider
+import com.jocmp.capy.articles.UnreadSortOrder
+import com.jocmp.capy.db.Database
+import com.jocmp.capy.fixtures.FeedFixture
+import com.jocmp.capy.persistence.ArticleRecords
+import com.jocmp.readerclient.Category
+import com.jocmp.readerclient.GoogleReader
+import com.jocmp.readerclient.Item
+import com.jocmp.readerclient.Item.Link
+import com.jocmp.readerclient.Item.Origin
+import com.jocmp.readerclient.Item.Summary
+import com.jocmp.readerclient.ItemRef
+import com.jocmp.readerclient.Stream
+import com.jocmp.readerclient.StreamItemIDsResult
+import com.jocmp.readerclient.StreamItemsContentsResult
+import com.jocmp.readerclient.Subscription
+import com.jocmp.readerclient.SubscriptionListResult
+import io.mockk.coEvery
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import okhttp3.Headers
+import okhttp3.Protocol
+import okhttp3.Request
+import okhttp3.ResponseBody.Companion.toResponseBody
+import retrofit2.Response
+import java.net.SocketTimeoutException
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+class ReaderAccountDelegateTest {
+    private val accountID = "777"
+    private val postToken = "alice/foobar123"
+    private lateinit var database: Database
+    private lateinit var googleReader: GoogleReader
+    private lateinit var feedFixture: FeedFixture
+    private lateinit var delegate: AccountDelegate
+
+    val subscriptions = listOf(
+        Subscription(
+            id = "feed/2",
+            title = "Ars Technica - All content",
+            categories = listOf(
+                Category(
+                    id = "user/1/label/Gadgets",
+                    label = "Gadgets"
+                )
+            ),
+            url = "https://feeds.arstechnica.com/arstechnica/index",
+            htmlUrl = "https://arstechnica.com",
+            iconUrl = "",
+        ),
+        Subscription(
+            id = "feed/3",
+            title = "The Verge",
+            categories = listOf(
+                Category(
+                    id = "user/1/label/All",
+                    label = "All"
+                )
+            ),
+            url = "https://www.theverge.com/rss/index.xml",
+            htmlUrl = "https://theverge.com",
+            iconUrl = "",
+        ),
+    )
+
+    private val items = listOf(
+        Item(
+            id = "tag:google.com,2005:reader/item/0000000000000010",
+            published = 1723806013,
+            title = "Rocket Report: ULA is losing engineers; SpaceX is launching every two days",
+            canonical = listOf(Link("https://arstechnica.com/?p=2043638")),
+            origin = Origin(
+                streamId = "feed/2",
+                title = "Ars Technica - All content",
+                htmlUrl = "https://arstechnica.com",
+            ),
+            summary = Summary("Summary - Welcome to Edition 7.07 of the Rocket Report! SpaceX has not missed a beat since the Federal Aviation Administration gave the company a green light to resume Falcon 9 launches after a failure last month."),
+            content = Item.Content("Content - Welcome to Edition 7.07 of the Rocket Report! SpaceX has not missed a beat since the Federal Aviation Administration gave the company a green light to resume Falcon 9 launches after a failure last month."),
+        )
+    )
+
+    @BeforeTest
+    fun setup() {
+        database = InMemoryDatabaseProvider.build(accountID)
+        feedFixture = FeedFixture(database)
+        googleReader = mockk()
+
+        delegate = ReaderAccountDelegate(database, googleReader)
+    }
+
+    @Test
+    fun refresh_updatesEntries() = runTest {
+        val itemRefs = listOf(ItemRef("16"))
+
+        stubSubscriptions()
+        stubUnread(itemRefs)
+        stubStarred()
+        stubReadingList(itemRefs)
+
+        delegate.refresh()
+
+        val articles = database
+            .articlesQueries
+            .countAll(read = false, starred = false)
+            .executeAsList()
+
+        val taggedNames = database
+            .feedsQueries
+            .tagged()
+            .executeAsList()
+            .map { it.name }
+
+        val feeds = database
+            .feedsQueries
+            .all()
+            .executeAsList()
+
+        assertEquals(expected = 2, actual = feeds.size)
+
+        assertEquals(expected = listOf("All", "Gadgets"), actual = taggedNames)
+
+        assertEquals(expected = 1, actual = articles.size)
+    }
+
+    @Test
+    fun refresh_findsMissingArticles() = runTest {
+        val itemRefs = listOf("1", "16").map { ItemRef(it) }
+
+        stubSubscriptions()
+        stubUnread(itemRefs)
+        stubStarred(listOf("1", "2").map { ItemRef(it) })
+
+        val unreadItem = Item(
+            id = "tag:google.com,2005:reader/item/0000000000000001",
+            published = 1708710158,
+            title = "Reddit admits more moderator protests could hurt its business",
+            canonical = listOf(Link("https://arstechnica.com/?p=2005526")),
+            author = "Scharon Harding",
+            origin = Origin(
+                streamId = "feed/2",
+                title = "Ars Technica - All content",
+                htmlUrl = "https://arstechnica.com",
+            ),
+            summary = Summary("Enlarge (credit: Jakub Porzycki/NurPhoto via Getty Images) Reddit filed to go public on Thursday (PDF), revealing various details of the social media company's inner workings. Among the revelations, Reddit acknowledged the threat of future user protests"),
+        )
+
+        val readItem = Item(
+            id = "tag:google.com,2005:reader/item/0000000000000002",
+            title = "Apple’s iPhone 16 launch event is set for September",
+            summary = Summary("Apple’s tagline: 'It’s Glowtime.'"),
+            canonical = listOf(Link("https://www.theverge.com/2024/8/26/24223957/apple-iphone-16-launch-event-date-glowtime")),
+            published = 1724521358,
+            author = "Jay Peters",
+            origin = Origin(
+                streamId = "feed/3",
+                title = "The Verge",
+                htmlUrl = "https://theverge.com",
+            ),
+        )
+
+        stubReadingList(itemRefs, listOf(unreadItem, items.first()))
+
+        val starredItems = listOf(unreadItem, readItem)
+
+        coEvery {
+            googleReader.streamItemsContents(starredItems.map { it.hexID }, postToken = postToken)
+        }.returns(Response.success(StreamItemsContentsResult(starredItems)))
+
+        delegate.refresh()
+
+        val starredArticles = ArticleRecords(database)
+            .byStatus
+            .all(
+                ArticleStatus.STARRED,
+                limit = 2,
+                offset = 0,
+                unreadSort = UnreadSortOrder.NEWEST_FIRST,
+            )
+            .executeAsList()
+
+        val unreadArticle = starredArticles.find { it.id == unreadItem.hexID }!!
+        val readArticle = starredArticles.find { it.id == readItem.hexID }!!
+
+        assertFalse(unreadArticle.read)
+        assertTrue(readArticle.read)
+    }
+
+    @Test
+    fun refresh_IOException() = runTest {
+        val networkError = SocketTimeoutException("Sorry networked charlie")
+        coEvery { googleReader.subscriptionList() }.throws(networkError)
+
+        val result = delegate.refresh()
+
+        assertEquals(result, Result.failure(networkError))
+    }
+
+    private fun stubSubscriptions(subscriptions: List<Subscription> = this.subscriptions) {
+        coEvery { googleReader.subscriptionList() }.returns(
+            Response.success(
+                SubscriptionListResult(
+                    subscriptions
+                )
+            )
+        )
+    }
+
+    private fun stubStarred(itemRefs: List<ItemRef> = emptyList()) {
+        coEvery {
+            googleReader.streamItemsIDs(
+                streamID = Stream.STARRED.id,
+            )
+        }.returns(
+            Response.success(
+                StreamItemIDsResult(
+                    itemRefs = itemRefs,
+                    continuation = null
+                )
+            )
+        )
+    }
+
+    private fun stubReadingList(itemRefs: List<ItemRef>, items: List<Item> = this.items) {
+        coEvery {
+            googleReader.streamItemsIDs(
+                streamID = Stream.READING_LIST.id,
+                since = any(),
+                count = 100,
+                excludedStreamID = Stream.READ.id
+            )
+        }.returns(Response.success(StreamItemIDsResult(itemRefs = itemRefs, continuation = null)))
+
+        val errorResponse = okhttp3.Response.Builder()
+            .code(401)
+            .protocol(Protocol.HTTP_1_1)
+            .headers(Headers.headersOf(GoogleReader.BAD_TOKEN_HEADER_KEY, "true"))
+            .message("Unauthorized")
+            .request(
+                Request.Builder().url("http://localhost/").build()
+            ).build()
+
+        coEvery {
+            googleReader.streamItemsContents(items.map { it.hexID }, postToken = null)
+        }.returns(Response.error("".toResponseBody(), errorResponse))
+
+        coEvery {
+            googleReader.token()
+        }.returns(Response.success(postToken))
+
+        coEvery {
+            googleReader.streamItemsContents(items.map { it.hexID }, postToken = postToken)
+        }.returns(Response.success(StreamItemsContentsResult(items)))
+    }
+
+    private fun stubUnread(itemRefs: List<ItemRef>) {
+        coEvery {
+            googleReader.streamItemsIDs(
+                streamID = Stream.READING_LIST.id,
+                count = 10_000,
+                excludedStreamID = Stream.READ.id,
+            )
+        }.returns(Response.success(StreamItemIDsResult(itemRefs = itemRefs, continuation = null)))
+    }
+}
diff --git a/readerclient/src/main/java/com/jocmp/readerclient/GoogleReader.kt b/readerclient/src/main/java/com/jocmp/readerclient/GoogleReader.kt
index 18319b72..63514da3 100644
--- a/readerclient/src/main/java/com/jocmp/readerclient/GoogleReader.kt
+++ b/readerclient/src/main/java/com/jocmp/readerclient/GoogleReader.kt
@@ -7,9 +7,10 @@ import retrofit2.Retrofit
 import retrofit2.converter.moshi.MoshiConverterFactory
 import retrofit2.converter.scalars.ScalarsConverterFactory
 import retrofit2.create
+import retrofit2.http.Field
+import retrofit2.http.FormUrlEncoded
 import retrofit2.http.GET
 import retrofit2.http.POST
-import retrofit2.http.Path
 import retrofit2.http.Query
 
 interface GoogleReader {
@@ -20,29 +21,23 @@ interface GoogleReader {
 
     @GET("reader/api/0/stream/items/ids")
     suspend fun streamItemsIDs(
-       @Query("s") streamID: String,
-       @Query("n") count: Int = 10_000,
-       @Query("xt") excludedStreamID: String? = null,
-       @Query("output") output: String = "json",
+        @Query("s") streamID: String,
+        /** Epoch timestamp. Items older than this timestamp are filtered out. */
+        @Query("ot") since: Long? = null,
+        @Query("c") continuation: String? = null,
+        @Query("n") count: Int = 10_000,
+        /** A stream ID to exclude from the list. */
+        @Query("xt") excludedStreamID: String? = null,
+        @Query("output") output: String = "json",
     ): Response<StreamItemIDsResult>
 
-    // use to fetch missing articles
+    @FormUrlEncoded
     @POST("reader/api/0/stream/items/contents")
     suspend fun streamItemsContents(
-//        @FormUrlEncoded
-    )
-
-    @GET("reader/api/0/stream/contents/{streamID}")
-    suspend fun streamContents(
-        @Path("streamID") streamID: String,
-        @Query("n") count: Int = 100,
-        /** Epoch timestamp. Items older than this timestamp are filtered out. */
-        @Query("ot") since: Long? = null,
-        /** A stream ID to exclude from the list. */
-        @Query("xt") excludedStreamID: String = Stream.READ.id,
-        @Query("c") continuation: String? = null,
+        @Field("i") ids: List<String>,
+        @Field("T") postToken: String?,
         @Query("output") output: String = "json",
-    ): Response<StreamContentsResult>
+    ): Response<StreamItemsContentsResult>
 
     @POST("accounts/ClientLogin")
     suspend fun clientLogin(
@@ -50,7 +45,12 @@ interface GoogleReader {
         @Query("Passwd") password: String
     ): Response<String>
 
+    @GET("reader/api/0/token")
+    suspend fun token(): Response<String>
+
     companion object {
+        const val BAD_TOKEN_HEADER_KEY = "X-Reader-Google-Bad-Token"
+
         fun create(
             client: OkHttpClient,
             baseURL: String
diff --git a/readerclient/src/main/java/com/jocmp/readerclient/Item.kt b/readerclient/src/main/java/com/jocmp/readerclient/Item.kt
index fe4f5ab5..1e8f6462 100644
--- a/readerclient/src/main/java/com/jocmp/readerclient/Item.kt
+++ b/readerclient/src/main/java/com/jocmp/readerclient/Item.kt
@@ -10,6 +10,7 @@ data class Item(
     val canonical: List<Link>,
     val summary: Summary,
     val origin: Origin,
+    val content: Content? = null,
     val author: String? = null,
     val enclosure: List<Enclosure>? = null,
 ) {
@@ -27,6 +28,11 @@ data class Item(
         val content: String,
     )
 
+    @JsonClass(generateAdapter = true)
+    data class Content(
+        val content: String,
+    )
+
     @JsonClass(generateAdapter = true)
     data class Enclosure(
         val href: String,
diff --git a/readerclient/src/main/java/com/jocmp/readerclient/ItemRef.kt b/readerclient/src/main/java/com/jocmp/readerclient/ItemRef.kt
new file mode 100644
index 00000000..728d91d9
--- /dev/null
+++ b/readerclient/src/main/java/com/jocmp/readerclient/ItemRef.kt
@@ -0,0 +1,10 @@
+package com.jocmp.readerclient
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class ItemRef(
+    val id: String,
+) {
+    val hexID = String.format("%016x", id.toLong())
+}
diff --git a/readerclient/src/main/java/com/jocmp/readerclient/Stream.kt b/readerclient/src/main/java/com/jocmp/readerclient/Stream.kt
index 3daaef04..f1b0b497 100644
--- a/readerclient/src/main/java/com/jocmp/readerclient/Stream.kt
+++ b/readerclient/src/main/java/com/jocmp/readerclient/Stream.kt
@@ -1,7 +1,6 @@
 package com.jocmp.readerclient
 
 enum class Stream(val id: String) {
-    KEPT_UNREAD("user/-/state/com.google/kept-unread"),
 
     READING_LIST("user/-/state/com.google/reading-list"),
 
diff --git a/readerclient/src/main/java/com/jocmp/readerclient/StreamItemIDsResult.kt b/readerclient/src/main/java/com/jocmp/readerclient/StreamItemIDsResult.kt
index 65f848df..22ff95c5 100644
--- a/readerclient/src/main/java/com/jocmp/readerclient/StreamItemIDsResult.kt
+++ b/readerclient/src/main/java/com/jocmp/readerclient/StreamItemIDsResult.kt
@@ -4,12 +4,6 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class StreamItemIDsResult(
-    val itemRefs: List<ItemRef>
-) {
-    @JsonClass(generateAdapter = true)
-    data class ItemRef(
-        val id: Long,
-    ) {
-        val hexID = String.format("%016x", id)
-    }
-}
+    val itemRefs: List<ItemRef>,
+    val continuation: String?,
+)
diff --git a/readerclient/src/main/java/com/jocmp/readerclient/StreamItemsContentsResult.kt b/readerclient/src/main/java/com/jocmp/readerclient/StreamItemsContentsResult.kt
new file mode 100644
index 00000000..2deaf229
--- /dev/null
+++ b/readerclient/src/main/java/com/jocmp/readerclient/StreamItemsContentsResult.kt
@@ -0,0 +1,8 @@
+package com.jocmp.readerclient
+
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class StreamItemsContentsResult(
+    val items: List<Item>,
+)
diff --git a/readerclient/src/main/java/com/jocmp/readerclient/Subscription.kt b/readerclient/src/main/java/com/jocmp/readerclient/Subscription.kt
index 868bc0b2..9b4a319d 100644
--- a/readerclient/src/main/java/com/jocmp/readerclient/Subscription.kt
+++ b/readerclient/src/main/java/com/jocmp/readerclient/Subscription.kt
@@ -4,7 +4,7 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class Subscription(
-   val id: String,
+    val id: String,
     val title: String,
     val categories: List<Category>,
     val url: String,