Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision e4119c22651ba589d6fceee5bc1802a11716f279)
+++ src/routes/activitypub.js	(revision 3155d8cc64d4ccbb40d068c206728faea3faee6a)
@@ -977,6 +977,14 @@
   const ouderLink = (pagina) => (pagina.more && pagina.oldest
     ? `${inboxId}?before=${encodeURIComponent(pagina.oldest)}${size}` : undefined);
-  if (req.query.before) {
-    const pagina = AP.inboxPage(auth.site.slug, { before: String(req.query.before), limit: req.query.limit });
+  // ?random=1 is dezelfde pagina, maar vanaf een willekeurige plek in je
+  // geschiedenis: met inbox-zero aan is 'ouder' alles wat je al doorgenomen
+  // hebt, en dan is een greep eruit zinniger dan een rij ervan. De pagina
+  // noemt zichzelf naar waar hij werkelijk begint, en `next` loopt vanaf daar
+  // gewoon door naar beneden.
+  if (req.query.before || req.query.random) {
+    const pagina = req.query.random
+      ? AP.inboxRandomPage(auth.site.slug, { limit: req.query.limit })
+      : AP.inboxPage(auth.site.slug, { before: String(req.query.before), limit: req.query.limit });
+    const vanaf = req.query.random ? pagina.start : String(req.query.before);
     const ids = (leg) => pagina.rows.filter((r) => r.leg === leg).map((r) => r.ref);
     const tl = new Map(AP.timelineRowsByIds(auth.site.slug, ids('timeline')).map((r) => [r.id, r]));
@@ -1006,5 +1014,5 @@
     return AP.sendAP(res, {
       '@context': AP.AP_CONTEXT,
-      id: `${inboxId}?before=${encodeURIComponent(String(req.query.before))}${size}`,
+      id: vanaf ? `${inboxId}?before=${encodeURIComponent(vanaf)}${size}` : `${inboxId}?random=1${size}`,
       type: 'OrderedCollectionPage',
       partOf: inboxId,
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision e4119c22651ba589d6fceee5bc1802a11716f279)
+++ src/services/ActivityPubService.js	(revision 3155d8cc64d4ccbb40d068c206728faea3faee6a)
@@ -55,5 +55,5 @@
   getTimeline, replyRowsByUri, timelineRowsByIds, getReplyMessages,
   feedCursor, feedChangesSince, waitForFeedChange,
-  conversationHeads, conversationHistory, inboxPage, messageRowsByUri,
+  conversationHeads, conversationHistory, inboxPage, inboxRandomPage, messageRowsByUri,
   readMarkers, markRead, unreadPerConversation, getDirectMessages,
   isoStamp, timelineAttachments, extractEmojiTags, gateAttachments,
@@ -64,5 +64,5 @@
   getTimeline, replyRowsByUri, timelineRowsByIds, getReplyMessages,
   feedCursor, feedChangesSince, waitForFeedChange,
-  conversationHeads, conversationHistory, inboxPage, messageRowsByUri,
+  conversationHeads, conversationHistory, inboxPage, inboxRandomPage, messageRowsByUri,
   readMarkers, markRead, unreadPerConversation, getDirectMessages,
   isoStamp, timelineAttachments, extractEmojiTags, gateAttachments,
@@ -4395,5 +4395,5 @@
   getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote, noteAudience, mayReadNote,
   listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
-  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineRowsByIds, contentWarning, getDirectMessages, readMarkers, markRead, unreadPerConversation, messageRowsByUri, replyRowsByUri, conversationHeads, conversationHistory, inboxPage, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
+  webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineRowsByIds, contentWarning, getDirectMessages, readMarkers, markRead, unreadPerConversation, messageRowsByUri, replyRowsByUri, conversationHeads, conversationHistory, inboxPage, inboxRandomPage, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
   acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision,
   gateOutgoingFollow, performApprovedFollow, recordGuardianEvent, listGuardianEvents, GUARDIAN_EVENT_KEEP,
Index: src/services/ap-timeline.js
===================================================================
--- src/services/ap-timeline.js	(revision e4119c22651ba589d6fceee5bc1802a11716f279)
+++ src/services/ap-timeline.js	(revision 3155d8cc64d4ccbb40d068c206728faea3faee6a)
@@ -402,4 +402,30 @@
 }
 
+/**
+ * Een pagina vanaf een willekeurige post in je geschiedenis (shaer-ewrt).
+ *
+ * Het anker is een TIJDLIJNRIJ, geen willekeurige rij uit de unie: de app
+ * toont posts in het raster, en een greep die op een stapel berichten landt
+ * laat niets zien. Vanaf daar is het een gewone pagina over alle poten, dus
+ * `start` is een echte cursor en `next` loopt gewoon door.
+ *
+ * `rnd` is er voor de toetsen.
+ */
+export function inboxRandomPage(slug, { limit = 60, rnd = Math.random } = {}) {
+  try {
+    const { n } = db.prepare('SELECT COUNT(*) AS n FROM ap_timeline WHERE slug = ?').get(slug);
+    const k = n ? Math.min(Math.floor(rnd() * n), n - 1) : 0;
+    if (k === 0) return { ...inboxPage(slug, { limit }), start: null };
+    // De rij er net boven: de pagina begint direct daaronder, met het anker
+    // als eerste post. Zelfde stempel en volgorde als de unie.
+    const boven = db.prepare(`
+      SELECT id AS ref, ${STEMPEL('COALESCE(published, created_at)')} AS stamp
+        FROM ap_timeline WHERE slug = ?
+       ORDER BY stamp DESC, ref DESC LIMIT 1 OFFSET ?`).get(slug, k - 1);
+    const start = cursorOf(boven);
+    return { ...inboxPage(slug, { before: start, limit }), start };
+  } catch { return { rows: [], more: false, oldest: null, start: null }; }
+}
+
 // De kolommen die een bericht tot kaart maken. Een constante, want de
 // gesprekslezing haalt dezelfde rows op: twee lijsten die uiteenlopen leveren
