Index: ripts/migrate-circles.mjs
===================================================================
--- scripts/migrate-circles.mjs	(revision 78b6d8a72b391c1960a8488dc735fb03495bc209)
+++ 	(revision )
@@ -1,42 +1,0 @@
-// One-time migration: convert the old Cirkels (pull-protocol circle_links) into
-// ActivityPub follows with auto-boost ("feature an artist"). Run per instance:
-//   cd ~/apps/<instance> && node scripts/migrate-circles.mjs
-// Idempotent: followActor upserts ap_following, so re-running is safe.
-
-import 'dotenv/config';
-import db from '../src/config/database.js';
-import ActivityPubService from '../src/services/ActivityPubService.js';
-
-// A Klonkt site's AP actor is reachable from its root via content negotiation:
-// an AP-Accept GET on the root 302s to /ap/users/<slug>.
-async function resolveActor(siteUrl) {
-  try {
-    const r = await fetch(siteUrl, { headers: { Accept: 'application/activity+json' }, redirect: 'manual' });
-    if (r.status >= 300 && r.status < 400) { const loc = r.headers.get('location'); if (loc) return new URL(loc, siteUrl).href; }
-    if (r.ok) return siteUrl; // root already serves the actor
-  } catch (e) { /* unreachable */ }
-  return null;
-}
-
-let links = [];
-try {
-  links = db.prepare(`
-    SELECT cl.remote_url AS url, s.id AS sid, s.slug AS slug
-    FROM circle_links cl JOIN sites s ON s.id = cl.local_site_id
-    WHERE cl.status != 'removed'
-  `).all();
-} catch (e) { console.log('no circle_links table — nothing to migrate'); process.exit(0); }
-
-if (!links.length) { console.log('no active circle_links — nothing to migrate'); process.exit(0); }
-
-for (const l of links) {
-  const actor = await resolveActor(l.url);
-  if (!actor) { console.log(`SKIP ${l.slug} -> ${l.url} (actor unresolvable)`); continue; }
-  try {
-    const r = await ActivityPubService.followActor({ id: l.sid, slug: l.slug }, actor, true);
-    console.log(`${l.slug} -> ${actor} : ${r && r.error ? 'ERR ' + r.error : 'OK (auto-boost)'}`);
-  } catch (e) {
-    console.log(`${l.slug} -> ${actor} : EXC ${e.message}`);
-  }
-}
-process.exit(0);
Index: src/server.js
===================================================================
--- src/server.js	(revision 78b6d8a72b391c1960a8488dc735fb03495bc209)
+++ src/server.js	(revision 4c47eff199fea2b744d9209d04948b8e7e59b1a6)
@@ -58,5 +58,5 @@
 import ogRoutes from './routes/og.js';
 import apRoutes from './routes/activitypub.js';
-import { apWants, startDeliveryWorker, autoMigrateCircles, selfHealTimeline } from './services/ActivityPubService.js';
+import { apWants, startDeliveryWorker, selfHealTimeline } from './services/ActivityPubService.js';
 
 // SESSION_SECRET: use the env var if set. Otherwise auto-generate a strong one
@@ -161,5 +161,4 @@
 startScheduler(); // release planning: publish scheduled posts when publish_at is reached
 startDeliveryWorker(); // retry failed fediverse deliveries with backoff
-autoMigrateCircles(); // one-time: convert legacy circle_links -> AP auto-boost follows
 selfHealTimeline(); // once per SELFHEAL_VERSION bump: re-sync the fediverse cache (covers/edits) after a drastic update
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 78b6d8a72b391c1960a8488dc735fb03495bc209)
+++ src/services/ActivityPubService.js	(revision 4c47eff199fea2b744d9209d04948b8e7e59b1a6)
@@ -1084,38 +1084,19 @@
 }
 
-// One-time, best-effort migration of the old Cirkels (pull-protocol circle_links)
-// into ActivityPub follows with auto-boost. Runs once per instance at boot so a
-// site that updates past the old protocol keeps its cirkel without a manual step.
+// Resolve a Klonkt/AP actor URL from a site root: a Klonkt site's root 302s to
+// /ap/users/<slug> (content negotiation; Location may be relative). Used by
+// followActor for bare-domain follows.
+// NB: the old auto-migration of legacy Cirkels (circle_links -> AP follows) was
+// REMOVED on 2026-06-26 — it auto-sent Follows on boot, which violates "the code
+// never throws anything into the fediverse automatically" (would surprise-Follow
+// for some operators at scale). The dead circle_links table stays as harmless dead
+// data; an operator restores an old cirkel by re-following in /volgend (their click).
 async function resolveApActor(siteUrl) {
   try {
     const r = await fetch(siteUrl, { headers: { Accept: 'application/activity+json' }, redirect: 'manual' });
-    // A Klonkt site's root 302s to /ap/users/<slug> (Location may be relative).
     if (r.status >= 300 && r.status < 400) { const loc = r.headers.get('location'); if (loc) return new URL(loc, siteUrl).href; }
     if (r.ok) return siteUrl;
   } catch { /* unreachable */ }
   return null;
-}
-let _circlesMigrating = false;
-export async function autoMigrateCircles() {
-  if (_circlesMigrating) return; _circlesMigrating = true;
-  try {
-    let done; try { done = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('circles_migrated_v2'); } catch { return; }
-    if (done && done.value === '1') return;
-    // Migrate EVERY link the user added (not only status='active'): the old
-    // 'error'/'outdated' statuses came from the pull-protocol's health checks
-    // (now irrelevant) — e.g. a peer that removed /.klonkt/* shows up as error
-    // but still has a working AP actor.
-    let links = [];
-    try { links = db.prepare("SELECT cl.remote_url AS url, s.id AS sid, s.slug AS slug FROM circle_links cl JOIN sites s ON s.id = cl.local_site_id WHERE cl.status != 'removed'").all(); } catch { /* no legacy table */ }
-    let ok = 0;
-    for (const l of links) {
-      try {
-        const actor = await resolveApActor(l.url);
-        if (actor) { const r = await followActor({ id: l.sid, slug: l.slug }, actor, true); if (!(r && r.error)) ok++; }
-      } catch { /* best-effort per link */ }
-    }
-    try { db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run('circles_migrated_v2', '1'); } catch { /* ignore */ }
-    if (links.length) console.log(`[AP] circle migration: ${ok}/${links.length} legacy link(s) -> auto-boost`);
-  } catch { /* never block boot */ } finally { _circlesMigrating = false; }
 }
 
@@ -1371,5 +1352,5 @@
   listOutbox, deliverOutboxDelete,
   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, getTimeline, sendInteraction,
-  autoBoostCount, boostedCount, markBoosted, unmarkBoosted, getCirkelPosts, getCirkelMembers, autoMigrateCircles, selfHealTimeline, boostLatestN,
+  autoBoostCount, boostedCount, markBoosted, unmarkBoosted, getCirkelPosts, getCirkelMembers, selfHealTimeline, boostLatestN,
   getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
   deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
