Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision e0a1ec1d9cfde10c3acb385c2a8c253a5501b6b9)
+++ src/services/ActivityPubService.js	(revision d3b9f6889eac39bb97f19770f90c27334db38e54)
@@ -23,4 +23,24 @@
 
 const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
+// Full JSON-LD context for every AP object we emit: AS2 core + security (publicKey) + the
+// extension terms we actually use (Mastodon/toot + schema.org), each with a term definition
+// so a strict JSON-LD processor resolves them instead of dropping them → valid AS2/JSON-LD.
+// This is the same context shape Mastodon publishes, so Mastodon sees no change.
+const AP_CONTEXT = [
+  'https://www.w3.org/ns/activitystreams',
+  'https://w3id.org/security/v1',
+  {
+    toot: 'http://joinmastodon.org/ns#',
+    schema: 'http://schema.org#',
+    sensitive: 'as:sensitive',
+    Hashtag: 'as:Hashtag',
+    manuallyApprovesFollowers: 'as:manuallyApprovesFollowers',
+    discoverable: 'toot:discoverable',
+    featured: { '@id': 'toot:featured', '@type': '@id' },
+    PropertyValue: 'schema:PropertyValue',
+    value: 'schema:value',
+    embedUrl: { '@id': 'schema:embedUrl', '@type': '@id' },
+  },
+];
 
 // Short random suffix so two activity ids minted in the same millisecond (e.g.
@@ -125,5 +145,5 @@
   const keys = getOrCreateKeys(site.slug);
   const actor = {
-    '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'],
+    '@context': AP_CONTEXT,
     id,
     type: 'Person',
@@ -351,5 +371,5 @@
   const note = buildNote(base, site, post);
   return {
-    '@context': 'https://www.w3.org/ns/activitystreams',
+    '@context': AP_CONTEXT,
     id: note.id + '#create',
     type: 'Create',
@@ -366,5 +386,5 @@
   const items = (posts || []).slice(0, MAX_OUTBOX).map((p) => buildCreate(base, site, p));
   return {
-    '@context': 'https://www.w3.org/ns/activitystreams',
+    '@context': AP_CONTEXT,
     id,
     type: 'OrderedCollection',
@@ -377,5 +397,5 @@
   const id = `${actorId(base, site.slug)}/followers`;
   return {
-    '@context': 'https://www.w3.org/ns/activitystreams',
+    '@context': AP_CONTEXT,
     id,
     type: 'OrderedCollection',
@@ -390,5 +410,5 @@
   const id = `${actorId(base, site.slug)}/following`;
   return {
-    '@context': 'https://www.w3.org/ns/activitystreams',
+    '@context': AP_CONTEXT,
     id,
     type: 'OrderedCollection',
@@ -405,5 +425,5 @@
   const items = (posts || []).map((p) => buildNote(base, site, p));
   return {
-    '@context': 'https://www.w3.org/ns/activitystreams',
+    '@context': AP_CONTEXT,
     id,
     type: 'OrderedCollection',
@@ -721,5 +741,5 @@
     const me = actorId(base, slug);
     const keys = getOrCreateKeys(slug);
-    const accept = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${me}#accept-${Date.now()}-${rid()}`, type: 'Accept', actor: me, object: act };
+    const accept = { '@context': AP_CONTEXT, id: `${me}#accept-${Date.now()}-${rid()}`, type: 'Accept', actor: me, object: act };
     deliver(remote.inbox, accept, `${me}#main-key`, keys.private_pem).catch((e) => console.warn('[AP] Accept delivery failed:', e.message));
     // Auto-backfill: send our recent posts as Create so the instance has our history
@@ -909,5 +929,5 @@
   const nid = noteId(base, post.id);
   const del = {
-    '@context': 'https://www.w3.org/ns/activitystreams',
+    '@context': AP_CONTEXT,
     id: `${nid}#delete-${Date.now()}-${rid()}`,
     type: 'Delete',
@@ -932,5 +952,5 @@
   note.updated = new Date().toISOString();
   const update = {
-    '@context': 'https://www.w3.org/ns/activitystreams',
+    '@context': AP_CONTEXT,
     id: `${noteId(base, post.id)}#update-${Date.now()}-${rid()}`,
     type: 'Update', actor: me, to: [PUBLIC], cc: [`${me}/followers`],
@@ -952,5 +972,5 @@
   const me = actorId(base, site.slug);
   const update = {
-    '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'],
+    '@context': AP_CONTEXT,
     id: `${me}#update-${Date.now()}-${rid()}`,
     type: 'Update', actor: me, to: [PUBLIC], cc: [`${me}/followers`],
@@ -976,5 +996,4 @@
   const keyId = `${me}#main-key`;
   const featured = `${me}/featured`;
-  const AS = 'https://www.w3.org/ns/activitystreams';
   const note = (id) => noteId(base, id);
   const pinned = db.prepare(
@@ -986,5 +1005,5 @@
   // 1. Remove every current pin so Mastodon can recreate them in order.
   for (const id of removeIds) {
-    const rm = { '@context': AS, id: `${me}#rm-${id}-${Date.now()}-${rid()}`, type: 'Remove', actor: me, object: note(id), target: featured, to: [PUBLIC] };
+    const rm = { '@context': AP_CONTEXT, id: `${me}#rm-${id}-${Date.now()}-${rid()}`, type: 'Remove', actor: me, object: note(id), target: featured, to: [PUBLIC] };
     for (const inbox of inboxes) deliver(inbox, rm, keyId, keys.private_pem).catch(() => { /* best-effort */ });
   }
@@ -993,5 +1012,5 @@
   // 2. Add in rank-DESC order, gaps so each StatusPin gets an increasing created_at.
   for (const p of pinned) {
-    const add = { '@context': AS, id: `${me}#add-${p.id}-${Date.now()}-${rid()}`, type: 'Add', actor: me, object: note(p.id), target: featured, to: [PUBLIC], cc: [`${me}/followers`] };
+    const add = { '@context': AP_CONTEXT, id: `${me}#add-${p.id}-${Date.now()}-${rid()}`, type: 'Add', actor: me, object: note(p.id), target: featured, to: [PUBLIC], cc: [`${me}/followers`] };
     for (const inbox of inboxes) deliver(inbox, add, keyId, keys.private_pem).catch(() => { /* best-effort */ });
     await new Promise((r) => setTimeout(r, 2000));
@@ -1146,5 +1165,5 @@
   const note = buildReplyNote(base, site, row);
   const create = {
-    '@context': 'https://www.w3.org/ns/activitystreams',
+    '@context': AP_CONTEXT,
     id: note.id + '#create', type: 'Create', actor: me,
     published: note.published, to: note.to, cc: note.cc, object: note,
@@ -1267,5 +1286,5 @@
     const me = actorId(base, site.slug);
     const nid = noteId(base, row.id);
-    const del = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${nid}#delete-${Date.now()}-${rid()}`, type: 'Delete', actor: me, to: [PUBLIC], object: { id: nid, type: 'Tombstone' } };
+    const del = { '@context': AP_CONTEXT, id: `${nid}#delete-${Date.now()}-${rid()}`, type: 'Delete', actor: me, to: [PUBLIC], object: { id: nid, type: 'Tombstone' } };
     const keys = getOrCreateKeys(site.slug);
     const inboxes = new Set();
@@ -1300,5 +1319,5 @@
   note.updated = new Date().toISOString();
   const update = {
-    '@context': 'https://www.w3.org/ns/activitystreams',
+    '@context': AP_CONTEXT,
     id: `${note.id}#update-${Date.now()}-${rid()}`, type: 'Update', actor: me,
     published: note.published, updated: note.updated, to: note.to, cc: note.cc, object: note,
@@ -1516,5 +1535,5 @@
   const followId = `${me}#follow-${Date.now()}-${rid()}`;
   fwStmts().ins.run(site.slug, actor.id, ai.handle, ai.name, ai.icon, ai.url, actor.inbox, followId, 'pending', autoBoost ? 1 : 0);
-  const follow = { '@context': 'https://www.w3.org/ns/activitystreams', id: followId, type: 'Follow', actor: me, object: actor.id };
+  const follow = { '@context': AP_CONTEXT, id: followId, type: 'Follow', actor: me, object: actor.id };
   try { await deliver(actor.inbox, follow, `${me}#main-key`, keys.private_pem); }
   catch (e) { console.warn('[AP] follow deliver failed:', e.message); }
@@ -1546,5 +1565,5 @@
   // rather than send an unmatchable one. Deliver durably via the retry queue.
   if (row && row.inbox && row.follow_id) {
-    const undo = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${me}/undo/${Date.now()}-${rid()}`, type: 'Undo', actor: me, object: { id: row.follow_id, type: 'Follow', actor: me, object: actorUri } };
+    const undo = { '@context': AP_CONTEXT, id: `${me}/undo/${Date.now()}-${rid()}`, type: 'Undo', actor: me, object: { id: row.follow_id, type: 'Follow', actor: me, object: actorUri } };
     deliverWithRetry(site.slug, row.inbox, undo, `${me}#main-key`, keys.private_pem);
   } else if (row && row.inbox) {
@@ -1576,5 +1595,5 @@
     const inner = kind === 'unboost' ? 'Announce' : 'Like';
     act = {
-      '@context': 'https://www.w3.org/ns/activitystreams',
+      '@context': AP_CONTEXT,
       id: `${me}/undo/${Date.now()}-${rid()}`, type: 'Undo', actor: me,
       object: { id: `${me}/${inner.toLowerCase()}/${Date.now()}-${rid()}`, type: inner, actor: me, object: targetNoteId },
@@ -1584,5 +1603,5 @@
     const type = kind === 'boost' ? 'Announce' : 'Like';
     act = {
-      '@context': 'https://www.w3.org/ns/activitystreams',
+      '@context': AP_CONTEXT,
       id: `${me}/${type.toLowerCase()}/${Date.now()}-${rid()}`,
       type, actor: me, object: targetNoteId,
@@ -1684,5 +1703,5 @@
 
 export default {
-  getOrCreateKeys, apWants, sendAP, actorId, noteId,
+  AP_CONTEXT, getOrCreateKeys, apWants, sendAP, actorId, noteId,
   buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
   followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
