Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 0688b5fcf8935a7e243f429c90946fd67027fb3c)
+++ src/services/ActivityPubService.js	(revision 1c2dcbaa5456f3903606d3d13a56c7661366fdda)
@@ -2194,4 +2194,39 @@
 }
 
+// Report a remote post or account to its home instance (moderation). Sends the Mastodon-standard
+// AS2 `Flag`: object = [reported account, reported status?], content = the reason, delivered to the
+// reported account's inbox so their instance's moderators receive it. objectUri = a post URL (its
+// author is resolved + included) OR pass actorUri to report an account directly.
+export async function sendReport(site, { objectUri, actorUri, reason }) {
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  if (!base || !site || !site.slug) return { error: 'config' };
+  let targetActor = actorUri || null;
+  let noteUri = null;
+  if (objectUri && /^https?:\/\//i.test(objectUri)) {
+    const note = await apGetJson(objectUri).catch(() => null);
+    if (note && note.id) { noteUri = note.id; if (!targetActor) targetActor = actorUriOf(note.attributedTo); }
+    else if (!targetActor) return { error: 'not_found' };
+  }
+  if (!targetActor || !/^https?:\/\//i.test(targetActor)) return { error: 'not_found' };
+  const actor = await fetchActor(targetActor).catch(() => null);
+  const inbox = actor && (actor.inbox || (actor.endpoints && actor.endpoints.sharedInbox)); // personal inbox → their moderators
+  if (!inbox) return { error: 'unreachable' };
+  const me = actorId(base, site.slug);
+  const keys = getOrCreateKeys(site.slug);
+  const object = [targetActor];
+  if (noteUri && noteUri !== targetActor) object.push(noteUri);
+  const flag = {
+    '@context': AP_CONTEXT,
+    id: `${me}#report-${Date.now()}-${rid()}`,
+    type: 'Flag',
+    actor: me,
+    content: String(reason == null ? '' : reason).slice(0, 3000),
+    object, // [account, status?] — Mastodon's Flag shape
+    to: [targetActor],
+  };
+  deliverWithRetry(site.slug, inbox, flag, `${me}#main-key`, keys.private_pem);
+  return { ok: true };
+}
+
 export function isBlockedAny(actorUri) {
   if (!actorUri) return false;
@@ -2249,5 +2284,5 @@
   listOutbox, deliverOutboxDelete, deliverOutboxUpdate,
   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, sendInteraction, voteOnPoll, voteOnRemotePoll,
-  parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread,
+  parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport,
   autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
   getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
