Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 75f28976575f764e9a39454c608c2d38d027500e)
+++ src/routes/activitypub.js	(revision aae5881d65b8532bd2291b2184bce662263e8752)
@@ -938,5 +938,15 @@
   const isWard = (() => { try { return Guardianship.listGuardians(auth.site.slug).length > 0; } catch { return false; } })();
   const uit = await AP.getThread(auth.site.slug, objectUri);
-  if (!uit.found) return res.status(404).json({ error: 'note not reachable' });
+  if (!uit.found) {
+    // WIENS schuld is dit? De oude melding zei "jouw server kon het niet
+    // laden" terwijl onze server het prima deed en de BRON weigerde -- dat
+    // wees naar de verkeerde partij (Barts melding, 10-8: een post van een
+    // account dat hij vanochtend nog volgde, en dat nu niet meer).
+    // 401/403/404/410 is een besluit van die server; al het andere, inclusief
+    // een status die we niet eens kregen, is een storing.
+    const geweigerd = [401, 403, 404, 410].includes(uit.sourceStatus);
+    return res.status(geweigerd ? 404 : 502)
+      .json({ error: geweigerd ? 'not shared by source' : 'source unreachable', sourceStatus: uit.sourceStatus || undefined });
+  }
   // De poortstand komt uit de kolom (shaer-9y2): expliciete 0/1 van de
   // guardians wint, de automatiek is dicht-voor-een-ward. Dicht is de KRING,
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 75f28976575f764e9a39454c608c2d38d027500e)
+++ src/services/ActivityPubService.js	(revision aae5881d65b8532bd2291b2184bce662263e8752)
@@ -3698,5 +3698,10 @@
   if (hit && Date.now() - hit.at < THREAD_VIEW_TTL_MS) return hit.out;
 
-  const get = (u) => localNoteObject(u, slug) || signedGetJson(slug, u);
+  // De status waarmee de BRON antwoordde op de note zelf. 401/403/404/410 is
+  // een besluit van die server (niet gedeeld, of weg); alles daarbuiten -- ook
+  // een stuk netwerk dat wegviel -- is een storing. De route moet dat verschil
+  // kunnen zeggen, anders wijst de melding naar de verkeerde partij.
+  let sourceStatus = 0;
+  const get = (u) => localNoteObject(u, slug) || signedGetJson(slug, u, (st) => { sourceStatus = st; });
   const note = await get(objectUri);
   const repliesRef = note && note.replies;
@@ -3781,5 +3786,5 @@
   })).sort((a, b) => String(a.published || '').localeCompare(String(b.published || '')));
 
-  const out = { notes, found: !!note };
+  const out = { notes, found: !!note, sourceStatus };
   threadViewCache.set(key, { at: Date.now(), out });
   if (threadViewCache.size > THREAD_VIEW_CACHE_MAX) {
@@ -5001,5 +5006,5 @@
  * (request-target) host date, the set verifyRequest checks.
  */
-async function signedGetJson(slug, url) {
+async function signedGetJson(slug, url, onStatus) {
   try {
     const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
@@ -5014,4 +5019,8 @@
     const sig = `keyId="${me}#main-key",algorithm="rsa-sha256",headers="(request-target) host date",signature="${signature}"`;
     const r = await safeFetch(url, { headers: { Accept: 'application/activity+json', Date: date, Signature: sig } });
+    // De status doorgeven aan wie erom vroeg: null alleen zegt "het lukte
+    // niet", en dat is te weinig om een WEIGERING van een STORING te
+    // onderscheiden. Wie geen callback meegeeft merkt hier niets van.
+    if (typeof onStatus === 'function') onStatus(r.status);
     if (!r.ok) return null;
     const len = Number(r.headers.get('content-length') || 0);
Index: test/thread.test.js
===================================================================
--- test/thread.test.js	(revision 75f28976575f764e9a39454c608c2d38d027500e)
+++ test/thread.test.js	(revision aae5881d65b8532bd2291b2184bce662263e8752)
@@ -193,2 +193,49 @@
   assert.ok(!na.attributedTo.tag, 'ook niet in de byline');
 });
+
+// ── Wiens schuld is het? ─────────────────────────────────────────────
+//
+// "Replies could not be loaded from your server" wees naar de verkeerde
+// partij: onze server deed het prima en de BRON weigerde. Een weigering
+// (401/403/404/410 -- niet gedeeld, of weg) is iets anders dan een storing, en
+// alleen de server weet welke van de twee het was.
+test('een weigering van de bron is 404, een storing is 502', async (t) => {
+  const crypto = await import('crypto');
+  const express = (await import('express')).default;
+  const routes = (await import('../src/routes/activitypub.js')).default;
+
+  const bearer = 'test-token-' + 'd'.repeat(24);
+  db.prepare('INSERT INTO oauth_tokens (token_hash, client_id, user_id, site_slug, scope) VALUES (?,?,?,?,?)')
+    .run(crypto.createHash('sha256').update(bearer).digest('base64url'), 'c', 'u1', 'kind', 'read write');
+
+  const app = express();
+  app.use(routes);
+  const server = app.listen(0);
+  t.after(() => server.close());
+  await new Promise((r) => server.once('listening', r));
+
+  const vorige = globalThis.fetch;
+  t.after(() => { globalThis.fetch = vorige; });
+  const vraag = async (uri) => {
+    const r = await echteFetch(
+      `http://127.0.0.1:${server.address().port}/ap/users/kind/thread?object=${encodeURIComponent(uri)}`,
+      { headers: { Authorization: `Bearer ${bearer}` } });
+    return { status: r.status, body: await r.json() };
+  };
+
+  // De bron zegt nee -- precies wat boiert.eu doet met een friends-only post
+  // voor iemand die de auteur niet (meer) volgt.
+  const DICHT = 'https://203.0.113.50/notes/dicht';
+  globalThis.fetch = async () => new Response('nope', { status: 404 });
+  const nee = await vraag(DICHT);
+  assert.equal(nee.status, 404);
+  assert.equal(nee.body.error, 'not shared by source');
+  assert.equal(nee.body.sourceStatus, 404);
+
+  // De bron ligt eruit. Dezelfde lege uitkomst, een andere waarheid.
+  const STUK = 'https://203.0.113.51/notes/stuk';
+  globalThis.fetch = async () => new Response('boem', { status: 500 });
+  const stuk = await vraag(STUK);
+  assert.equal(stuk.status, 502);
+  assert.equal(stuk.body.error, 'source unreachable');
+});
