Index: src/routes/federation.js
===================================================================
--- src/routes/federation.js	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
+++ src/routes/federation.js	(revision f63cbc22ee20c4ac230e3564c560021a8e7fdec4)
@@ -8,5 +8,5 @@
 
 import express from 'express';
-import { buildActor, buildOutbox, signBody } from '../services/CircleFederation.js';
+import { buildActor, buildOutbox, signBody, KLONKT_PROTO, MIN_PROTO } from '../services/CircleFederation.js';
 import { getTenancy } from '../services/SettingsService.js';
 
@@ -18,9 +18,17 @@
 }
 
+// De proto die de consument zegt te draaien (uit z'n request-header), of 0.
+function consumerProto(req) {
+  return parseInt(req.get('Klonkt-Proto') || '0', 10) || 0;
+}
+
 router.get('/.klonkt/actor.json', (req, res) => {
   // Cirkels = solo-naar-solo; hubs publiceren geen federatie-actor.
   if (getTenancy() === 'hub') return res.status(404).type('text/plain').send('Niet beschikbaar in hub-modus');
+  // De actor serveren we ALTIJD (ook aan oudere consumenten) zodat zij onze proto
+  // kunnen lezen en een nette "update vereist"-melding kunnen tonen.
   const body = JSON.stringify(buildActor(baseUrl(req)), null, 2);
   res.type('application/activity+json; charset=utf-8');
+  res.set('Klonkt-Proto', String(KLONKT_PROTO));
   res.set('Cache-Control', 'public, max-age=300');
   res.send(body);
@@ -29,4 +37,13 @@
 router.get('/.klonkt/outbox.json', (req, res) => {
   if (getTenancy() === 'hub') return res.status(404).type('text/plain').send('Niet beschikbaar in hub-modus');
+  res.set('Klonkt-Proto', String(KLONKT_PROTO));
+  // Te-oude consument? Weiger met 426 Upgrade Required (de crypto-binding sluit 'm
+  // sowieso al uit; dit geeft een expliciet, leesbaar signaal). proto 0 = geen
+  // header (bv. een browser/curl) → toestaan, die verifieert toch niet.
+  const cp = consumerProto(req);
+  if (cp && cp < MIN_PROTO) {
+    return res.status(426).type('text/plain')
+      .send(`Upgrade Required: deze cirkel draait proto ${KLONKT_PROTO}; jouw Klonkt (proto ${cp}) is te oud.`);
+  }
   const body = JSON.stringify(buildOutbox(baseUrl(req)), null, 2);
   res.type('application/activity+json; charset=utf-8');
Index: src/services/CircleFederation.js
===================================================================
--- src/services/CircleFederation.js	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
+++ src/services/CircleFederation.js	(revision f63cbc22ee20c4ac230e3564c560021a8e7fdec4)
@@ -15,4 +15,19 @@
 import db from '../config/database.js';
 import { getSetting, setSetting } from './SettingsService.js';
+
+// ── Protocol-versie (federatie) ───────────────────────────────
+// KLONKT_PROTO zit IN de ondertekende grondslag (zie signingInput): een instance
+// die niet op deze proto draait kan onze getekende outbox NIET verifiëren, en wij
+// de hare niet. Bijblijven is dus geen beleefde check die je wegpatcht, maar
+// cryptografisch afgedwongen — de enige manier om mee te doen is dezelfde proto
+// draaien (= de update). Bump KLONKT_PROTO bij elke release die federatie/security
+// raakt, en koppel een securityfix aan elke bump → outdated = buiten + onveilig.
+// MIN_PROTO = de laagste proto waarmee we nog federeren.
+export const KLONKT_PROTO = 2;
+export const MIN_PROTO = 2;
+
+function signingInput(proto, body) {
+  return `klonkt/proto/${proto}\n${body}`;
+}
 
 // ── Sleutelbeheer ─────────────────────────────────────────────
@@ -37,17 +52,18 @@
 }
 
-/** Tekent een exacte body-string met de instance-privésleutel (Ed25519). */
-export function signBody(rawString) {
+/** Tekent een body-string, gebonden aan de protocol-versie (Ed25519). */
+export function signBody(rawString, proto = KLONKT_PROTO) {
   const key = crypto.createPrivateKey(getKeys().priv);
-  return crypto.sign(null, Buffer.from(rawString, 'utf8'), key).toString('base64');
+  return crypto.sign(null, Buffer.from(signingInput(proto, rawString), 'utf8'), key).toString('base64');
 }
 
-/** Verifieert een body tegen een SPKI-DER-base64 publieke sleutel (voor sync/tests). */
-export function verifyBody(rawString, sigB64, pubDerB64) {
+/** Verifieert een body tegen een SPKI-DER-base64 publieke sleutel, voor de gegeven
+ *  proto. Een mismatch in proto = mismatch in grondslag = ongeldige handtekening. */
+export function verifyBody(rawString, sigB64, pubDerB64, proto = KLONKT_PROTO) {
   try {
     const key = crypto.createPublicKey({
       key: Buffer.from(pubDerB64, 'base64'), format: 'der', type: 'spki',
     });
-    return crypto.verify(null, Buffer.from(rawString, 'utf8'), key, Buffer.from(sigB64, 'base64'));
+    return crypto.verify(null, Buffer.from(signingInput(proto, rawString), 'utf8'), key, Buffer.from(sigB64, 'base64'));
   } catch {
     return false;
@@ -101,5 +117,5 @@
       publicKeyBase64: getPublicKeyB64(),
     },
-    klonkt: { version: 1, allowCircle: allowsCircle(site) },
+    klonkt: { version: 1, proto: KLONKT_PROTO, allowCircle: allowsCircle(site) },
   };
 }
@@ -111,5 +127,5 @@
   const empty = {
     '@context': 'https://www.w3.org/ns/activitystreams',
-    type: 'OrderedCollection', id, totalItems: 0, orderedItems: [],
+    type: 'OrderedCollection', id, totalItems: 0, orderedItems: [], klonkt: { proto: KLONKT_PROTO },
   };
   if (!allowsCircle(site)) return empty;
@@ -148,4 +164,5 @@
     '@context': 'https://www.w3.org/ns/activitystreams',
     type: 'OrderedCollection', id, totalItems: orderedItems.length, orderedItems,
+    klonkt: { proto: KLONKT_PROTO },
   };
 }
Index: src/services/CircleService.js
===================================================================
--- src/services/CircleService.js	(revision 1b4d5dd5a81a498bc61db716ba5ba3d2bbdd64b2)
+++ src/services/CircleService.js	(revision f63cbc22ee20c4ac230e3564c560021a8e7fdec4)
@@ -6,5 +6,5 @@
 
 import db from '../config/database.js';
-import { verifyBody } from './CircleFederation.js';
+import { verifyBody, KLONKT_PROTO, MIN_PROTO } from './CircleFederation.js';
 import { getTenancy } from './SettingsService.js';
 
@@ -25,4 +25,13 @@
 function baseOf(remoteUrl) {
   return String(remoteUrl).replace(/\/+$/, '');
+}
+
+// Bron buiten de cirkel zetten met een leesbare reden (geen stille mislukking).
+// Aparte status 'outdated' zodat de Beheer-UI er een nette "update vereist"-
+// melding van kan maken i.p.v. een generieke fout.
+function markOutdated(link, msg) {
+  db.prepare("UPDATE circle_links SET status='outdated', last_error=?, last_synced=CURRENT_TIMESTAMP WHERE id=?")
+    .run(String(msg).slice(0, 300), link.id);
+  return { ok: false, outdated: true, link: link.remote_url, error: msg };
 }
 
@@ -36,5 +45,9 @@
       signal: ac.signal,
       redirect: 'follow',
-      headers: { Accept: 'application/activity+json, application/json' },
+      headers: {
+        Accept: 'application/activity+json, application/json',
+        // Vertel de publisher onze proto → die kan ons met 426 weren als we te oud zijn.
+        'Klonkt-Proto': String(KLONKT_PROTO),
+      },
     });
     if (!res.ok) throw new Error(`HTTP ${res.status}`);
@@ -84,4 +97,18 @@
   if (originOf(actorId) !== originOf(actorUrl)) throw new Error('actor.id heeft andere origin dan de actor-URL');
 
+  // Protocol-versie-gate. De proto zit óók in de outbox-handtekening-grondslag,
+  // dus liegen in de (ongetekende) actor helpt niet: bij een echte mismatch faalt
+  // de verificatie verderop alsnog. Hier vooral voor een DUIDELIJKE melding +
+  // buitensluiten zonder stille mislukking.
+  const remoteProto = Number(actor.klonkt && actor.klonkt.proto) || 1;
+  if (remoteProto > KLONKT_PROTO) {
+    return markOutdated(link,
+      `Jouw Klonkt (proto ${KLONKT_PROTO}) is ouder dan ${base} (proto ${remoteProto}). Werk je eigen instance bij om te blijven federeren.`);
+  }
+  if (remoteProto < MIN_PROTO) {
+    return markOutdated(link,
+      `${base} draait een oudere Klonkt (proto ${remoteProto}; minimaal ${MIN_PROTO} vereist). Vraag ze te updaten.`);
+  }
+
   // TOFU: een sleutelwissel vereist expliciete herbevestiging (anti-hijack)
   const existing = db.prepare('SELECT public_key FROM remote_actors WHERE id = ?').get(actorId);
@@ -104,5 +131,5 @@
   const sigHeader = o.headers.get('klonkt-signature') || '';
   const sig = (sigHeader.match(/ed25519=(.+)\s*$/) || [])[1];
-  if (!sig || !verifyBody(o.text, sig, pubKey)) {
+  if (!sig || !verifyBody(o.text, sig, pubKey, remoteProto)) {
     throw new Error('outbox-handtekening ongeldig of ontbreekt');
   }
