Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 55eca8b82c97abcf07ed1086c1b4cf60905cc7a8)
+++ src/routes/activitypub.js	(revision 1a2f20637fb8d9ab79622a1dc8b82aed707b6eef)
@@ -121,4 +121,25 @@
   }
   AP.sendAP(res, ob, audience === 'friend' ? 'private, no-store' : undefined);
+});
+
+// ── Follow-QR (Robins verzoek, 31-7) ──────────────────────────────
+// A PNG QR of share:social/follow/AP/@slug@host: the ward shows it in
+// Account, a friend scans the SCREEN with the ordinary camera app and their
+// Shaer opens with the follow question. Public on purpose: it encodes only
+// the public handle, and the app's plain image loaders carry no bearer.
+router.get('/ap/users/:slug/follow-qr.png', async (req, res) => {
+  const site = db.prepare('SELECT slug FROM sites WHERE slug = ?').get(req.params.slug);
+  if (!site) return res.status(404).end();
+  try {
+    const host = new URL(baseUrl(req)).host;
+    const { default: QRCode } = await import('qrcode');
+    const png = await QRCode.toBuffer(`share:social/follow/AP/@${site.slug}@${host}`, { width: 600, margin: 1 });
+    res.set('Content-Type', 'image/png');
+    res.set('Cache-Control', 'public, max-age=86400');
+    res.send(png);
+  } catch (e) {
+    console.warn('[AP] follow-qr failed:', e && e.message);
+    res.status(500).end();
+  }
 });
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 55eca8b82c97abcf07ed1086c1b4cf60905cc7a8)
+++ src/services/ActivityPubService.js	(revision 1a2f20637fb8d9ab79622a1dc8b82aed707b6eef)
@@ -2383,5 +2383,8 @@
         const actorUri = c2sIdOf(object);
         if (!actorUri) return { status: 400, error: 'missing_object' };
-        await followActor(site, actorUri);
+        // The error REACHES the app (Robins melding, 31-7): swallowing it
+        // made a failed follow look exactly like a successful one.
+        const r = await followActor(site, actorUri);
+        if (r && r.error) return { status: 502, error: 'follow_failed', detail: r.error };
         return { status: 202, url: actorUri };
       }
@@ -3645,5 +3648,8 @@
   else actorUrl = null;
   if (!actorUrl) return { error: 'not_found' };
-  const actor = await fetchActor(actorUrl).catch(() => null);
+  // SIGNED, as this actor: an authorized-fetch instance refuses an anonymous
+  // GET of the actor doc, which made following from a boost silently fail
+  // (Robins melding, 31-7). Signed, the other side sees who asks.
+  const actor = await signedGetJson(site.slug, actorUrl);
   if (!actor || !actor.id || !actor.inbox) return { error: 'unreachable' };
   const ai = actorInfo(actor, actor.id);
@@ -3660,4 +3666,28 @@
   // Follow + feature in one step → backfill their recent posts into the Cirkel right away.
   if (autoBoost) backfillFromOutbox(site.slug, actor.id).catch(() => {});
+  // A ward's guardians are TOLD about a new follow (Robins verzoek, 31-7):
+  // a follow brings new content into the child's feed, and the village
+  // should know the door opened. A direct note per guardian, best-effort;
+  // FEP-633c 5.3 gates inbound follows, the outbound notice is Shaer policy
+  // for now (bead: spec-vraag).
+  try {
+    const guardians = Guardianship.listGuardians(site.slug);
+    if (guardians.length) {
+      const meRef = actorId(base, site.slug);
+      const esc = (t) => String(t).replace(/[<>&]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]));
+      const label = esc(ai.name || ai.handle || actor.id);
+      for (const g of guardians) {
+        const note = {
+          id: `${meRef}/follow-notice/${Date.now().toString(36)}${rid()}`,
+          type: 'Note', attributedTo: meRef, to: [g.other_uri],
+          tag: [{ type: 'Mention', href: g.other_uri }],
+          content: `<p>👀 ${esc(site.title || site.slug)} is now following ${label}.</p>`,
+        };
+        deliverToActor(site, g.other_uri, { id: `${note.id}#create`, type: 'Create', actor: meRef, to: [g.other_uri], object: note })
+          .catch(() => { /* retried by the queue */ });
+      }
+      console.log('[AP] follow notice →', guardians.length, 'guardian(s) of', site.slug);
+    }
+  } catch { /* geen guardians is geen fout */ }
   return { ok: true, name: ai.name, handle: ai.handle, actor: actor.id };
 }
