Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 155c24e735f57cb57efc79bdb088c83281ecaa1a)
+++ src/config/database.js	(revision 7922694b27fd0b9c421adfef80ab3c17ddde3a35)
@@ -444,4 +444,7 @@
   ensureColumn('ap_outbox', 'to_actors', 'TEXT');   // JSON array of recipient actor URIs for direct notes
   ensureColumn('ap_outbox', 'help_request', 'INTEGER'); // FEP-633c shaer:helpRequest (ward's call for help)
+  ensureColumn('ap_followers', 'name', 'TEXT');    // cached display name (shaer-aa3)
+  ensureColumn('ap_followers', 'handle', 'TEXT');  // @user@host
+  ensureColumn('ap_followers', 'icon', 'TEXT');    // avatar URL
 }
 
Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 155c24e735f57cb57efc79bdb088c83281ecaa1a)
+++ src/routes/activitypub.js	(revision 7922694b27fd0b9c421adfef80ab3c17ddde3a35)
@@ -175,5 +175,6 @@
   if (!site) return res.status(404).end();
   if (owner) {
-    const items = db.prepare('SELECT actor_uri FROM ap_followers WHERE slug = ? ORDER BY created_at').all(site.slug).map((r) => r.actor_uri);
+    const uris = db.prepare('SELECT actor_uri FROM ap_followers WHERE slug = ? ORDER BY created_at').all(site.slug).map((r) => r.actor_uri);
+    const items = uris.map((u) => AP.buildActorRef(site.slug, u));   // name + avatar (shaer-aa3)
     return AP.sendAP(res, AP.buildFollowers(baseUrl(req), site, items.length, items));
   }
@@ -190,5 +191,5 @@
   if (owner) {
     let items = [];
-    try { items = db.prepare("SELECT actor_uri FROM ap_following WHERE slug = ? AND status = 'accepted' ORDER BY created_at").all(site.slug).map((r) => r.actor_uri); } catch { /* table may not exist */ }
+    try { items = db.prepare("SELECT actor_uri FROM ap_following WHERE slug = ? AND status = 'accepted' ORDER BY created_at").all(site.slug).map((r) => AP.buildActorRef(site.slug, r.actor_uri)); } catch { /* table may not exist */ }
     return AP.sendAP(res, AP.buildFollowing(baseUrl(req), site, items.length, items));
   }
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 155c24e735f57cb57efc79bdb088c83281ecaa1a)
+++ src/services/ActivityPubService.js	(revision 7922694b27fd0b9c421adfef80ab3c17ddde3a35)
@@ -621,8 +621,9 @@
 
 // ── followers store (lazy stmts) ──────────────────────────────────
-let _insF, _delF, _listF, _cntF;
+let _insF, _updFDisp, _delF, _listF, _cntF;
 function fStmts() {
   if (!_insF) {
-    _insF = db.prepare('INSERT OR IGNORE INTO ap_followers (slug, actor_uri, inbox, shared_inbox, created_at) VALUES (?,?,?,?,CURRENT_TIMESTAMP)');
+    _insF = db.prepare('INSERT OR IGNORE INTO ap_followers (slug, actor_uri, inbox, shared_inbox, name, handle, icon, created_at) VALUES (?,?,?,?,?,?,?,CURRENT_TIMESTAMP)');
+    _updFDisp = db.prepare('UPDATE ap_followers SET name = COALESCE(?, name), handle = COALESCE(?, handle), icon = COALESCE(?, icon) WHERE slug = ? AND actor_uri = ?');
     _delF = db.prepare('DELETE FROM ap_followers WHERE slug = ? AND actor_uri = ?');
     _listF = db.prepare('SELECT inbox, shared_inbox FROM ap_followers WHERE slug = ?');
@@ -646,4 +647,36 @@
   const info = db.prepare('DELETE FROM ap_followers WHERE slug = ? AND id = ?').run(slug, id);
   return info.changes > 0;
+}
+
+// Best cached display for an actor URI, across the caches Klonkt already fills:
+// followers (now with name/icon), following, interactions, timeline, mentions.
+// Falls back to a handle derived from the URI. Display info is not sensitive.
+export function actorDisplay(slug, uri) {
+  const ok = (r) => r && (r.name || r.icon);
+  try {
+    let r = db.prepare('SELECT name, handle, icon FROM ap_followers WHERE slug = ? AND actor_uri = ?').get(slug, uri);
+    if (ok(r)) return { name: r.name, handle: r.handle || deriveHandle(uri), icon: r.icon };
+    r = db.prepare('SELECT name, handle, icon FROM ap_following WHERE slug = ? AND actor_uri = ?').get(slug, uri);
+    if (ok(r)) return { name: r.name, handle: r.handle || deriveHandle(uri), icon: r.icon };
+    r = db.prepare('SELECT actor_name AS name, actor_handle AS handle, actor_icon AS icon FROM ap_interactions WHERE actor_uri = ? AND (actor_name IS NOT NULL OR actor_icon IS NOT NULL) ORDER BY created_at DESC LIMIT 1').get(uri);
+    if (ok(r)) return { name: r.name, handle: r.handle || deriveHandle(uri), icon: r.icon };
+    r = db.prepare('SELECT author_name AS name, author_handle AS handle, author_icon AS icon FROM ap_timeline WHERE author_uri = ? AND (author_name IS NOT NULL OR author_icon IS NOT NULL) LIMIT 1').get(uri);
+    if (ok(r)) return { name: r.name, handle: r.handle || deriveHandle(uri), icon: r.icon };
+    r = db.prepare('SELECT actor_name AS name, actor_handle AS handle, actor_icon AS icon FROM ap_mentions WHERE actor_uri = ? AND (actor_name IS NOT NULL OR actor_icon IS NOT NULL) ORDER BY created_at DESC LIMIT 1').get(uri);
+    if (ok(r)) return { name: r.name, handle: r.handle || deriveHandle(uri), icon: r.icon };
+  } catch { /* ignore */ }
+  return { name: null, handle: deriveHandle(uri), icon: null };
+}
+
+// AS2 actor reference with display, for the owner C2S followers/following view.
+// preferredUsername = the local part of the handle; name = the set display name.
+export function buildActorRef(slug, uri) {
+  const d = actorDisplay(slug, uri);
+  const user = d.handle && d.handle[0] === '@' ? d.handle.slice(1).split('@')[0] : null;
+  const out = { id: uri, type: 'Person' };
+  if (d.name) out.name = d.name;
+  if (user) out.preferredUsername = user;
+  if (d.icon) out.icon = { type: 'Image', url: d.icon };
+  return out;
 }
 
@@ -1255,5 +1288,7 @@
     if (!remote || !remote.inbox) return 202; // can't reach them → drop quietly
     const sharedInbox = (remote.endpoints && remote.endpoints.sharedInbox) || null;
-    fStmts().ins.run(slug, who, remote.inbox, sharedInbox);
+    const fi = actorInfo(remote, who);   // cache display for the friends list (shaer-aa3)
+    fStmts().ins.run(slug, who, remote.inbox, sharedInbox, fi.name, fi.handle, fi.icon);
+    try { _updFDisp.run(fi.name, fi.handle, fi.icon, slug, who); } catch { /* best effort */ }
     const me = actorId(base, slug);
     const keys = getOrCreateKeys(slug);
@@ -3035,4 +3070,4 @@
   linkifyBody, bakePostContent, bakePostContentWithMentions, listFollowers, removeFollower, listConnections,
   noteVisibility, isRejectedObject, rejectInteraction, interactionReportTarget,
-  getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility,
+  getMessages, notificationsSeenAt, ingestOutboxActivity, c2sVisibility, actorDisplay, buildActorRef,
 };
Index: test/c2s-contacts.test.js
===================================================================
--- test/c2s-contacts.test.js	(revision 7922694b27fd0b9c421adfef80ab3c17ddde3a35)
+++ test/c2s-contacts.test.js	(revision 7922694b27fd0b9c421adfef80ab3c17ddde3a35)
@@ -0,0 +1,45 @@
+// Owner followers/following carry display (name + avatar), not bare URIs
+// (shaer-aa3): name -> preferredUsername -> id.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const AP = (await import('../src/services/ActivityPubService.js')).default;
+
+test('a followed account resolves to its cached name + icon', () => {
+  db.prepare(`INSERT INTO ap_following (slug, actor_uri, handle, name, icon, status) VALUES (?,?,?,?,?,?)`)
+    .run('me', 'https://r.test/u/anna', '@anna@r.test', 'Anna de Vries', 'https://r.test/anna.png', 'accepted');
+  const ref = AP.buildActorRef('me', 'https://r.test/u/anna');
+  assert.equal(ref.id, 'https://r.test/u/anna');
+  assert.equal(ref.name, 'Anna de Vries');
+  assert.equal(ref.preferredUsername, 'anna');
+  assert.equal(ref.icon.url, 'https://r.test/anna.png');
+});
+
+test('a follower with cached display shows it', () => {
+  db.prepare(`INSERT INTO ap_followers (slug, actor_uri, name, handle, icon) VALUES (?,?,?,?,?)`)
+    .run('me', 'https://q.test/u/ben', 'Ben', '@ben@q.test', 'https://q.test/ben.png');
+  const ref = AP.buildActorRef('me', 'https://q.test/u/ben');
+  assert.equal(ref.name, 'Ben');
+  assert.equal(ref.preferredUsername, 'ben');
+});
+
+test('an unknown actor falls back to id + derived username, no name', () => {
+  const ref = AP.buildActorRef('me', 'https://x.test/users/cas');
+  assert.equal(ref.id, 'https://x.test/users/cas');
+  assert.equal(ref.name, undefined);            // no invented name
+  assert.equal(ref.preferredUsername, 'cas');   // from the handle
+});
+
+test('interaction cache is used when a follower has no own display row', () => {
+  db.prepare(`INSERT INTO ap_followers (slug, actor_uri) VALUES ('me','https://r.test/u/dana')`).run();
+  db.prepare(`INSERT INTO ap_interactions (kind, post_id, actor_uri, actor_name, actor_handle, actor_icon, created_at)
+    VALUES ('like','p','https://r.test/u/dana','Dana','@dana@r.test','https://r.test/dana.png',CURRENT_TIMESTAMP)`).run();
+  const ref = AP.buildActorRef('me', 'https://r.test/u/dana');
+  assert.equal(ref.name, 'Dana');
+});
