Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 3b43e4c42e059a39a2ea3ecd827e68d6c1d8a4e6)
+++ src/services/ActivityPubService.js	(revision 06bae1e8e638673eeb003f2120df40c66abe7a71)
@@ -2506,6 +2506,27 @@
   if (type === 'Accept' && act.object) {
     const fid = typeof act.object === 'string' ? act.object : (act.object && act.object.id);
-    if (fid) { try { fwStmts().acc.run(fid); } catch { /* ignore */ } }
-    console.log('[AP] follow accepted', actorUri);
+    let raak = 0;
+    if (fid) { try { raak = fwStmts().acc.run(fid).changes; } catch { /* ignore */ } }
+    // TERUGVAL, en die is nodig gebleken tegen Funkwhale. Een Accept hoort de
+    // Follow terug te geven die hij beantwoordt, maar Funkwhale verzint er een
+    // EIGEN id voor, in ONZE namespace:
+    //
+    //   wij stuurden   .../ap/users/dev#follow-1786161977286-bb2de32f
+    //   Funkwhale zegt .../ap/users/dev#follows/19fd8b00-8f66-...
+    //
+    // Matchen op follow_id raakt dan niets, en de volgrelatie bleef eeuwig op
+    // 'pending' staan terwijl de logregel 'accepted' riep -- een stille no-op
+    // die pas opviel toen er nooit iets binnenkwam.
+    //
+    // Het paar dat we WEL zeker weten is (deze site, deze actor): de Accept is
+    // handtekening-geverifieerd, en actorUri is de ondertekenaar. Alleen een
+    // rij die nog op pending staat wordt geraakt, dus dit kan niets anders
+    // openzetten dan een follow die wij zelf hebben verstuurd.
+    if (!raak && slugParam && actorUri) {
+      try { raak = fwStmts().accByActor.run(slugParam, actorUri).changes; } catch { /* ignore */ }
+    }
+    // Eerlijk loggen: zonder treffer is er niets geaccepteerd, en dat hoort te
+    // zien te zijn in plaats van als succes voorbij te komen.
+    console.log('[AP] follow', raak ? 'accepted' : 'accept UNMATCHED', actorUri, fid ? '(' + fid + ')' : '');
     // The moment a friendship exists is the moment the history comes along
     // (Robins besluit, 30-7): delivery cannot reach into the past, so the
@@ -3725,5 +3746,5 @@
 }
 
-let _insFw, _delFw, _listFw, _accFw, _oneFw, _setAB;
+let _insFw, _delFw, _listFw, _accFw, _accFwByActor, _oneFw, _setAB;
 function fwStmts() {
   if (!_insFw) {
@@ -3732,8 +3753,12 @@
     _listFw = db.prepare('SELECT * FROM ap_following WHERE slug = ? ORDER BY created_at DESC');
     _accFw = db.prepare("UPDATE ap_following SET status = 'accepted' WHERE follow_id = ?");
+    // Terugval als de Accept ons follow-id niet teruggeeft (zie de Accept-tak
+    // in handleInbox): dan is het paar dat we WEL zeker weten (deze site, deze
+    // actor) genoeg, mits de rij nog op pending staat.
+    _accFwByActor = db.prepare("UPDATE ap_following SET status = 'accepted' WHERE slug = ? AND actor_uri = ? AND status = 'pending'");
     _oneFw = db.prepare('SELECT * FROM ap_following WHERE slug = ? AND actor_uri = ?');
     _setAB = db.prepare('UPDATE ap_following SET auto_boost = ? WHERE slug = ? AND actor_uri = ?');
   }
-  return { ins: _insFw, del: _delFw, list: _listFw, acc: _accFw, one: _oneFw, setAB: _setAB };
+  return { ins: _insFw, del: _delFw, list: _listFw, acc: _accFw, accByActor: _accFwByActor, one: _oneFw, setAB: _setAB };
 }
 export function listFollowing(slug) { return fwStmts().list.all(slug); }
Index: test/follow-accept-match.test.js
===================================================================
--- test/follow-accept-match.test.js	(revision 06bae1e8e638673eeb003f2120df40c66abe7a71)
+++ test/follow-accept-match.test.js	(revision 06bae1e8e638673eeb003f2120df40c66abe7a71)
@@ -0,0 +1,80 @@
+// Een Accept op onze Follow moet de volgrelatie openzetten -- ook als de
+// tegenpartij ons follow-id niet teruggeeft.
+//
+// Aanleiding: Funkwhale (audio.pepemoss.com, 7 augustus). Wij stuurden een
+// Follow met id `<actor>#follow-<ts>-<rnd>`; de Accept kwam terug met een door
+// Funkwhale zelf verzonnen id in onze namespace, `<actor>#follows/<uuid>`.
+// Matchen op follow_id raakte niets, de rij bleef op 'pending', en de logregel
+// riep toch 'accepted'. De relatie kwam nooit tot stand en er kwam dus ook
+// nooit muziek binnen.
+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;
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'u1', 'u1@t', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_public) VALUES (?,?,?,?,1)')
+  .run('s1', 'dev', 'Dev', 'u1');
+
+const REMOTE = 'https://audio.example/federation/actors/kanaal';
+const ONS_ID = 'https://test.example/ap/users/dev#follow-1786161977286-bb2de32f';
+
+function zetPending(followId = ONS_ID, actor = REMOTE) {
+  db.prepare('DELETE FROM ap_following').run();
+  db.prepare(`INSERT INTO ap_following (slug, actor_uri, handle, name, icon, url, inbox, follow_id, status, auto_boost, created_at)
+              VALUES (?,?,?,?,?,?,?,?,'pending',0,CURRENT_TIMESTAMP)`)
+    .run('dev', actor, '@kanaal@audio.example', 'Kanaal', null, null, actor + '/inbox', followId);
+}
+const status = (actor = REMOTE) =>
+  (db.prepare('SELECT status FROM ap_following WHERE slug = ? AND actor_uri = ?').get('dev', actor) || {}).status;
+
+// handleInbox draait de handtekeningcontrole; die is hier niet het onderwerp.
+// De Accept-tak krijgt een geverifieerde actor mee, precies zoals in bedrijf.
+const accept = (object) => AP.handleInbox({
+  body: { '@context': 'https://www.w3.org/ns/activitystreams', type: 'Accept', actor: REMOTE, object },
+  headers: {}, get: () => undefined, socket: {},
+}, 'dev', { id: REMOTE });
+
+test('de gewone weg: de Accept geeft ONS follow-id terug', async () => {
+  zetPending();
+  await accept({ id: ONS_ID, type: 'Follow', actor: 'https://test.example/ap/users/dev', object: REMOTE });
+  assert.equal(status(), 'accepted');
+});
+
+test('de Funkwhale-vorm: een zelfverzonnen id in onze namespace', async () => {
+  zetPending();
+  await accept({
+    id: 'https://test.example/ap/users/dev#follows/19fd8b00-8f66-4362-b233-542eadfa40fc',
+    type: 'Follow', actor: 'https://test.example/ap/users/dev', object: REMOTE,
+  });
+  assert.equal(status(), 'accepted', 'de terugval op (site, actor) hoort dit op te vangen');
+});
+
+test('een Accept als kale string blijft werken', async () => {
+  zetPending();
+  await accept(ONS_ID);
+  assert.equal(status(), 'accepted');
+});
+
+test('een Accept van een ANDERE actor raakt onze rij niet', async () => {
+  zetPending();
+  await AP.handleInbox({
+    body: { type: 'Accept', actor: 'https://elders.example/users/vreemd', object: { id: 'https://elders.example/x', type: 'Follow' } },
+    headers: {}, get: () => undefined, socket: {},
+  }, 'dev', { id: 'https://elders.example/users/vreemd' });
+  assert.equal(status(), 'pending', 'alleen de ondertekenaar zelf kan zijn eigen follow openzetten');
+});
+
+test('een al geaccepteerde rij wordt niet opnieuw geraakt door een vreemde Accept', async () => {
+  zetPending();
+  db.prepare('UPDATE ap_following SET status = ?').run('accepted');
+  await accept({ id: 'https://test.example/ap/users/dev#follows/anders', type: 'Follow', object: REMOTE });
+  assert.equal(status(), 'accepted');
+});
