Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 1c2dcbaa5456f3903606d3d13a56c7661366fdda)
+++ src/services/ActivityPubService.js	(revision 737ea05350ed2cb26c87fffc0f5a108905f63c7e)
@@ -936,5 +936,5 @@
   // Blocked actor/domain → silently drop (202, don't reveal the block).
   if (claimedActor && isBlockedAny(claimedActor)) { console.log('[AP] inbox dropped (blocked)', claimedActor, 'from', ip); return 202; }
-  const GATED = ['Create', 'Like', 'Announce', 'Follow', 'Delete', 'Undo', 'Accept', 'Reject', 'Add', 'Remove', 'Update'];
+  const GATED = ['Create', 'Like', 'Announce', 'Follow', 'Delete', 'Undo', 'Accept', 'Reject', 'Add', 'Remove', 'Update', 'Flag'];
   if (GATED.includes(type)) {
     if (!verified || !claimedActor || verified.id !== claimedActor) {
@@ -942,4 +942,30 @@
       return 401;
     }
+  }
+
+  // A moderation report (Flag) about our content — store it for the targeted site's owner
+  // (each Klonkt site is moderated by its own owner). Signature is enforced (GATED).
+  if (type === 'Flag') {
+    const objs = Array.isArray(act.object) ? act.object : (act.object ? [act.object] : []);
+    const objectUris = objs.map((o) => (typeof o === 'string' ? o : (o && o.id))).filter(Boolean);
+    let targetSlug = null;
+    const noteIds = [];
+    for (const u of objectUris) {
+      const s = slugFromActorUrl(u);        // one of our actors?
+      if (s) { targetSlug = targetSlug || s; continue; }
+      const pid = postIdFromNoteUrl(u, base); // one of our notes?
+      if (pid) noteIds.push(pid);
+    }
+    if (!targetSlug && noteIds.length) {
+      try { const r = db.prepare('SELECT s.slug FROM posts p JOIN sites s ON s.id = p.site_id WHERE p.id = ? LIMIT 1').get(noteIds[0]); if (r) targetSlug = r.slug; } catch { /* ignore */ }
+    }
+    if (!targetSlug) return 202; // not about us / can't tell → drop
+    const ai = actorInfo(await resolveActor(claimedActor).catch(() => null), claimedActor);
+    try {
+      db.prepare('INSERT INTO ap_reports (slug, actor_uri, actor_name, actor_handle, actor_icon, content, objects, created_at) VALUES (?,?,?,?,?,?,?,CURRENT_TIMESTAMP)')
+        .run(targetSlug, claimedActor || null, ai.name, ai.handle, ai.icon, HtmlSanitizerService.toPlainText(act.content || '').slice(0, 3000), JSON.stringify(objectUris.slice(0, 20)));
+      console.log('[AP] report received for', targetSlug, 'from', claimedActor);
+    } catch { /* ignore */ }
+    return 202;
   }
 
@@ -2110,4 +2136,9 @@
     });
   } catch { /* ignore */ }
+  try {
+    for (const r of db.prepare('SELECT actor_uri, actor_name, actor_handle, actor_icon, content, created_at FROM ap_reports WHERE slug = ? ORDER BY created_at DESC LIMIT 50').all(slug)) {
+      out.push({ type: 'report', name: r.actor_name, handle: r.actor_handle, url: r.actor_uri, icon: r.actor_icon, content: r.content, created_at: r.created_at });
+    }
+  } catch { /* ignore */ }
   out.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
   return out.slice(0, limit || 60);
