Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision ad6f62af71f052ed32116666c62fefa7a06be1ac)
+++ src/routes/activitypub.js	(revision c66cbb4536b30530330e60aecad2a6de103f0375)
@@ -79,13 +79,22 @@
 
 // ── Outbox ────────────────────────────────────────────────────────
-router.get('/ap/users/:slug/outbox', (req, res) => {
+router.get('/ap/users/:slug/outbox', async (req, res) => {
   const site = publicSite(req.params.slug);
   if (!site) return res.status(404).end();
+  // Authorized fetch (FEP-633c §5.3 note): a committed guardian doing a SIGNED
+  // GET may read the ward's fan-only history too, without appearing as a
+  // follower. Unsigned / non-guardian callers get the public collection only.
+  let asGuardian = false;
+  if (req.headers['signature']) {
+    const verified = await AP.verifyRequest(req).catch(() => null);
+    asGuardian = !!(verified && AP.isWardGuardian(req.params.slug, verified.id));
+  }
+  const fanClause = asGuardian ? '' : "AND (fan_only IS NULL OR fan_only = 0)";
   const posts = db.prepare(
     `SELECT id, slug, title, content, cover_image_url, cover_video_url, nsfw, content_warning, published_at, created_at
-     FROM posts WHERE site_id = ? AND status = 'published' AND (fan_only IS NULL OR fan_only = 0)
+     FROM posts WHERE site_id = ? AND status = 'published' ${fanClause}
      ORDER BY COALESCE(published_at, created_at) DESC LIMIT 20`
   ).all(site.id);
-  AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, posts));
+  AP.sendAP(res, AP.buildOutbox(baseUrl(req), site, posts), asGuardian ? 'private, no-store' : undefined);
 });
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision ad6f62af71f052ed32116666c62fefa7a06be1ac)
+++ src/services/ActivityPubService.js	(revision c66cbb4536b30530330e60aecad2a6de103f0375)
@@ -144,7 +144,8 @@
 
 const AP_CONTENT_TYPE = 'application/activity+json; charset=utf-8';
-export function sendAP(res, obj) {
+export function sendAP(res, obj, cacheControl) {
   res.type(AP_CONTENT_TYPE);
-  res.set('Cache-Control', 'public, max-age=120');
+  // A per-caller (e.g. guardian-widened) view must not be publicly cached.
+  res.set('Cache-Control', cacheControl || 'public, max-age=120');
   res.send(JSON.stringify(obj));
 }
@@ -2901,4 +2902,11 @@
 }
 
+// FEP-633c §5.3 note (authorized fetch): true when `actorUri` is a committed
+// guardian of the local ward `wardSlug` — so a signed GET from it may read the
+// ward's non-public history without the guardian appearing as a follower.
+export function isWardGuardian(wardSlug, actorUri) {
+  try { return !!Guardianship.getRelation(wardSlug, 'ward', actorUri); } catch { return false; }
+}
+
 // FEP-633c §5.3: the guardians approved a gated follow of their ward. Send the
 // Accept to the follower and record them, so delivery (incl. followers-only)
@@ -3248,5 +3256,5 @@
   listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, timelineAttachments, sendInteraction, voteOnPoll, voteOnRemotePoll,
-  acceptGatedFollow, rejectGatedFollow,
+  acceptGatedFollow, rejectGatedFollow, isWardGuardian,
   parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
   autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
Index: test/authorized-fetch.test.js
===================================================================
--- test/authorized-fetch.test.js	(revision c66cbb4536b30530330e60aecad2a6de103f0375)
+++ test/authorized-fetch.test.js	(revision c66cbb4536b30530330e60aecad2a6de103f0375)
@@ -0,0 +1,23 @@
+// FEP-633c §5.3 note: a committed guardian is recognised for authorized fetch.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const AP = (await import('../src/services/ActivityPubService.js')).default;
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@t', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
+// Commit a guardian relation: kid (ward) is guarded by mom.
+const MOM = 'https://mom.example/ap/users/mom';
+db.prepare("INSERT INTO ap_guardianships (slug, role, other_uri, status, created_at) VALUES ('kid','ward',?, 'accepted', CURRENT_TIMESTAMP)").run(MOM);
+
+test('a committed guardian is recognised; a stranger is not', () => {
+  assert.equal(AP.isWardGuardian('kid', MOM), true);
+  assert.equal(AP.isWardGuardian('kid', 'https://x.example/ap/users/stranger'), false);
+  assert.equal(AP.isWardGuardian('nosuch', MOM), false);
+});
