Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 5a6a457ca2bf625de84ec03d20f7757480337fde)
+++ src/routes/activitypub.js	(revision d7526bda1a7c629688e73f463847832f685c3381)
@@ -12,8 +12,11 @@
  */
 import express from 'express';
+import { readFileSync } from 'fs';
 import db from '../config/database.js';
 import AP from '../services/ActivityPubService.js';
 
 const router = express.Router();
+let _ver = '1.0.0';
+try { _ver = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url))).version || _ver; } catch { /* keep default */ }
 
 const baseUrl = (req) => (process.env.PUBLIC_BASE_URL || `${req.protocol}://${req.get('host')}`).replace(/\/+$/, '');
@@ -85,4 +88,40 @@
 });
 
+// ── Replies collection ── lets remote servers fetch a post's whole thread.
+router.get('/ap/notes/:id/replies', (req, res) => {
+  const base = baseUrl(req);
+  const items = AP.getReplyUris(base, req.params.id);
+  AP.sendAP(res, {
+    '@context': 'https://www.w3.org/ns/activitystreams',
+    id: `${base}/ap/notes/${req.params.id}/replies`,
+    type: 'OrderedCollection',
+    totalItems: items.length,
+    orderedItems: items,
+  });
+});
+
+// ── NodeInfo ── standard instance metadata so fediverse tools recognise Klonkt.
+router.get('/.well-known/nodeinfo', (req, res) => {
+  res.type('application/json');
+  res.set('Cache-Control', 'public, max-age=3600');
+  res.send(JSON.stringify({ links: [{ rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', href: `${baseUrl(req)}/nodeinfo/2.1` }] }));
+});
+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 { /* */ }
+  try { posts = db.prepare("SELECT COUNT(*) c FROM posts WHERE status = 'published'").get().c; } catch { /* */ }
+  res.type('application/json; charset=utf-8');
+  res.set('Cache-Control', 'public, max-age=600');
+  res.send(JSON.stringify({
+    version: '2.1',
+    software: { name: 'klonkt', version: _ver, repository: 'https://github.com/roboburr/klonkt' },
+    protocols: ['activitypub'],
+    services: { inbound: [], outbound: [] },
+    openRegistrations: false,
+    usage: { users: { total: users }, localPosts: posts },
+    metadata: { nodeName: 'Klonkt' },
+  }));
+});
+
 // ── Inbox — Follow→Accept, Undo Follow (best-effort signature verify) ──
 const apJson = express.json({
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 5a6a457ca2bf625de84ec03d20f7757480337fde)
+++ src/services/ActivityPubService.js	(revision d7526bda1a7c629688e73f463847832f685c3381)
@@ -153,7 +153,20 @@
     cc: [`${aId}/followers`],
     tag: Array.isArray(post.tags) ? post.tags.map((t) => ({ type: 'Hashtag', name: '#' + String(t).replace(/\s+/g, '') })) : [],
+    replies: `${id}/replies`,
   };
   if (attachment.length) note.attachment = attachment;
   return note;
+}
+
+// All reply note URIs on a local post (inbound fediverse replies + our own
+// outbound replies) — backs the Note's `replies` Collection so remote servers
+// can fetch the whole thread.
+export function getReplyUris(base, postId) {
+  const out = [];
+  try {
+    for (const r of db.prepare("SELECT object_uri FROM ap_interactions WHERE kind = 'reply' AND post_id = ? AND object_uri != '' ORDER BY created_at").all(postId)) out.push(r.object_uri);
+    for (const r of db.prepare('SELECT id FROM ap_outbox WHERE post_id = ? ORDER BY rowid').all(postId)) out.push(`${base}/ap/notes/${r.id}`);
+  } catch { /* non-fatal */ }
+  return out;
 }
 
@@ -906,3 +919,4 @@
   getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
   deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
+  getReplyUris,
 };
