Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision fb6819d9b1cd4a5ebb58fbdaee318ac224ee4e40)
+++ src/routes/activitypub.js	(revision f2796d3e767524a548ef7b2f0dd228c23f75e650)
@@ -46,7 +46,13 @@
   res.type('application/jrd+json; charset=utf-8');
   res.set('Cache-Control', 'public, max-age=300');
+  const actorUri = AP.actorId(baseUrl(req), site.slug);
+  const profileUrl = baseUrl(req) + (site.slug === primarySlug() ? '/' : `/user/${encodeURIComponent(site.slug)}`);
   res.send(JSON.stringify({
     subject: `acct:${site.slug}@${hostOf(req)}`,
-    links: [{ rel: 'self', type: 'application/activity+json', href: AP.actorId(baseUrl(req), site.slug) }],
+    aliases: [actorUri, profileUrl],
+    links: [
+      { rel: 'self', type: 'application/activity+json', href: actorUri },
+      { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: profileUrl },
+    ],
   }));
 });
@@ -83,4 +89,13 @@
   const n = db.prepare('SELECT COUNT(*) n FROM ap_followers WHERE slug = ?').get(site.slug).n;
   AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, n));
+});
+
+// ── Following (count only) ────────────────────────────────────────
+router.get('/ap/users/:slug/following', (req, res) => {
+  const site = publicSite(req.params.slug);
+  if (!site) return res.status(404).end();
+  let n = 0;
+  try { n = db.prepare("SELECT COUNT(*) n FROM ap_following WHERE slug = ? AND status = 'accepted'").get(site.slug).n; } catch { /* table may not exist */ }
+  AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, n));
 });
 
@@ -151,5 +166,6 @@
 router.get('/nodeinfo/2.1', (req, res) => {
   let users = 0; let posts = 0;
-  try { users = db.prepare('SELECT COUNT(*) c FROM users').get().c; } catch { /* */ }
+  // "users" = public AP actors (sites), not the admin/member account rows.
+  try { users = db.prepare('SELECT COUNT(*) c FROM sites WHERE (is_public IS NULL OR is_public = 1)').get().c; } catch { /* */ }
   try { posts = db.prepare("SELECT COUNT(*) c FROM posts WHERE status = 'published'").get().c; } catch { /* */ }
   res.type('application/json; charset=utf-8');
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision fb6819d9b1cd4a5ebb58fbdaee318ac224ee4e40)
+++ src/services/ActivityPubService.js	(revision f2796d3e767524a548ef7b2f0dd228c23f75e650)
@@ -137,4 +137,5 @@
     outbox: `${id}/outbox`,
     followers: `${id}/followers`,
+    following: `${id}/following`,
     featured: `${id}/featured`,
     endpoints: { sharedInbox: `${base}/ap/inbox` },
@@ -149,4 +150,22 @@
     actor.icon = { type: 'Image', url: u };
   }
+  // Account creation date — shown by Mastodon + read by indexers (additive, standard AS2).
+  if (site.created_at) { try { actor.published = new Date(site.created_at).toISOString(); } catch { /* skip bad date */ } }
+  // Profile links → PropertyValue rows: Mastodon/PeerTube/WordPress-ActivityPub render these as
+  // profile metadata (rel=me enables link-back verification). Additive; ignored by simpler receivers.
+  try {
+    const links = JSON.parse(site.profile_links || '[]');
+    if (Array.isArray(links) && links.length) {
+      const esc = (s) => String(s).replace(/[<>&]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[c]));
+      const rows = links
+        .filter((l) => l && l.url && /^https?:/i.test(l.url))
+        .map((l) => ({
+          type: 'PropertyValue',
+          name: esc(l.platform || 'Link'),
+          value: `<a href="${esc(l.url).replace(/"/g, '&quot;')}" rel="me nofollow noopener" target="_blank">${esc(String(l.url).replace(/^https?:\/\//, ''))}</a>`,
+        }));
+      if (rows.length) actor.attachment = rows;
+    }
+  } catch { /* skip malformed profile_links */ }
   return actor;
 }
@@ -340,4 +359,17 @@
 }
 
+// The accounts this site follows — count only, mirroring buildFollowers. The spec lists
+// `following` as a standard actor property; Hubzilla/Friendica + crawlers expect it.
+export function buildFollowing(base, site, count) {
+  const id = `${actorId(base, site.slug)}/following`;
+  return {
+    '@context': 'https://www.w3.org/ns/activitystreams',
+    id,
+    type: 'OrderedCollection',
+    totalItems: count || 0,
+    orderedItems: [], // count only
+  };
+}
+
 // Pinned posts → the actor's `featured` collection. Mastodon reads this and shows
 // these as the "Featured" tab (pinned to the profile). Posts come ordered by pin
@@ -1483,7 +1515,13 @@
   const keys = getOrCreateKeys(site.slug);
   const row = fwStmts().one.get(site.slug, actorUri);
-  if (row && row.inbox) {
-    const undo = { '@context': 'https://www.w3.org/ns/activitystreams', id: `${me}#unfollow-${Date.now()}-${rid()}`, type: 'Undo', actor: me, object: { id: row.follow_id || `${me}#follow`, type: 'Follow', actor: me, object: actorUri } };
-    try { await deliver(row.inbox, undo, `${me}#main-key`, keys.private_pem); } catch { /* best-effort */ }
+  // Undo(Follow) MUST reference the original Follow's real id so the remote can correlate it
+  // and drop the follow. The old `${me}#follow` fallback never matched anything → the unfollow
+  // silently failed on the remote. With no stored follow id (legacy row), skip the network Undo
+  // 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 } };
+    deliverWithRetry(site.slug, row.inbox, undo, `${me}#main-key`, keys.private_pem);
+  } else if (row && row.inbox) {
+    console.warn('[AP] unfollow', site.slug, '→', actorUri, '— no stored follow id; removed locally only (legacy follow, remote may keep it)');
   }
   fwStmts().del.run(site.slug, actorUri);
@@ -1621,5 +1659,5 @@
 export default {
   getOrCreateKeys, apWants, sendAP, actorId, noteId,
-  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFeatured,
+  buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
   followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
   getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
