Index: docs/ward-outbound-follows-design.md
===================================================================
--- docs/ward-outbound-follows-design.md	(revision 3d882bdbd7d45a727ffdcb64eacb870baecdb05d)
+++ docs/ward-outbound-follows-design.md	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -89,25 +89,42 @@
   toestemming" kan laten zien in plaats van een tegel die er al volgend uitziet.
 
+## Status
+
+Gebouwd op 3-8-2026: `ap_pending_outgoing_follows` + `outgoing.js`, de poort in
+`ingestOutboxActivity` (`case 'Follow'`), de `outgoingFollows`-wachtrij, en het
+antwoord van de guardian op `POST /api/outgoing-follow/:id`. Negen tests in
+`test/outgoing-follow-gate.test.js`.
+
 ## Open vragen
 
-1. **Wat doen we met followers van vóór de adoptie?** Was de ward eerst een vrije
-   actor, dan zijn zijn bestaande followers nooit door een guardian gezien. Bij de
-   regel hierboven worden dat stuk voor stuk automatisch goed te keuren doelen.
-   Ofwel we accepteren dat, ofwel `ap_followers` krijgt een markering "door de
-   poort gekomen" en alleen die telt mee. Dit is de belangrijkste vraag van dit
-   document.
+1. **Beantwoord (Bart, 3-8): bestaande followers worden gegrandfatherd.**
+   `ap_followers` heeft nu `gate_approved`, gezet zodra de inkomende poort
+   iemand toelaat. Iedereen die al volgde op het moment dat de kolom erbij kwam,
+   krijgt de markering eenmalig mee: de regel is exact vanaf dat moment, in
+   plaats van met terugwerkende kracht wantrouwig tegen relaties die er al
+   waren. Wie daarna binnenkomt zonder poort — de followers van een vrije actor
+   die later ward wordt — telt niet mee voor de wederkerigheid.
 
-2. **Mag de ward zijn eigen verzoek intrekken** zolang het in de wacht staat? Lijkt
-   vanzelfsprekend ja, maar het is een Undo op iets dat nooit verstuurd is.
+2. **Nog open. Mag de ward zijn eigen verzoek intrekken** zolang het in de wacht
+   staat? `outgoing.withdraw()` bestaat al, maar er is nog geen route en geen
+   knop. Lijkt vanzelfsprekend ja, maar het is een Undo op iets dat nooit
+   verstuurd is.
 
-3. **De guardian zelf als doel.** Een ward die zijn eigen guardian volgt, hoort
-   niet te hoeven wachten. Dat is dezelfde uitzondering als inkomend, waar de
-   Follow van een vastgelegde guardian de poort overslaat.
+3. **Beantwoord: de guardian zelf als doel wacht niet.** Dezelfde uitzondering
+   als inkomend, waar de Follow van een vastgelegde guardian de poort overslaat.
+   Gebouwd en getest.
 
-4. **Interactie met Block/Orbit.** Blokkeert de ward iemand terwijl er nog een
-   verzoek voor die persoon open staat, dan moet dat verzoek verdwijnen.
+4. **Nog open. Interactie met Block/Orbit.** Blokkeert de ward iemand terwijl er
+   nog een verzoek voor die persoon open staat, dan moet dat verzoek verdwijnen.
+   `withdraw()` is er klaar voor; het wordt alleen nog nergens aangeroepen.
 
-5. **Emancipatie.** Wat gebeurt er met openstaande verzoeken als de
+5. **Nog open. Emancipatie.** Wat gebeurt er met openstaande verzoeken als de
    guardianship eindigt? Automatisch goedkeuren of laten vervallen.
+
+6. **Nieuw, uit het bouwen. Er zit geen venster op een verzoek.** Een uitgaande
+   follow die niemand beantwoordt blijft staan tot iemand hem beantwoordt —
+   dezelfde omissie die de handshake had voordat er een week op kwam (§3.5). Een
+   kind dat vraagt of het iemand mag volgen en nooit antwoord krijgt, verdient
+   een afloop.
 
 ## Raakvlakken met andere beads
Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 3d882bdbd7d45a727ffdcb64eacb870baecdb05d)
+++ src/config/database.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -58,4 +58,29 @@
   )`);
   db.exec(`CREATE TABLE IF NOT EXISTS ap_pending_follow_approvals (
+    follow_id TEXT NOT NULL,
+    guardian_uri TEXT NOT NULL,
+    decision TEXT NOT NULL,
+    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY (follow_id, guardian_uri)
+  )`);
+  // FEP-633c §5.3, the OTHER direction (shaer-p729): a ward's own follow is
+  // held until its guardians approve. Deliberately not ap_pending_follows —
+  // that table is keyed with the ward as the TARGET ("who wants to follow me"),
+  // and adding a direction column would make every existing query ambiguous.
+  db.exec(`CREATE TABLE IF NOT EXISTS ap_pending_outgoing_follows (
+    id TEXT PRIMARY KEY,
+    ward_slug TEXT NOT NULL,
+    target_uri TEXT NOT NULL,
+    target_inbox TEXT,
+    target_name TEXT,
+    target_handle TEXT,
+    target_icon TEXT,
+    quorum TEXT DEFAULT 'any',
+    status TEXT DEFAULT 'pending',
+    created_at TEXT DEFAULT CURRENT_TIMESTAMP
+  )`);
+  db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_ap_outgoing_follows_target
+           ON ap_pending_outgoing_follows(ward_slug, target_uri)`);
+  db.exec(`CREATE TABLE IF NOT EXISTS ap_outgoing_follow_approvals (
     follow_id TEXT NOT NULL,
     guardian_uri TEXT NOT NULL,
@@ -693,4 +718,18 @@
   ensureColumn('ap_outbox', 'away_until', 'INTEGER'); // FEP-633c 3.6.1 shaer:away + endTime (epoch ms)
   ensureColumn('ap_gated_offers', 'proposer', 'TEXT'); // who proposed (5.6): the settle-answer goes back to them
+  // Did a guardian actually say yes to this follower? That is what makes the
+  // mutual shortcut sound: a ward may follow back anyone its guardians already
+  // admitted, without asking the same question twice. Only follows that came
+  // through the §5.3 gate carry the mark; a free actor's followers never faced
+  // one. Everyone already following when this column arrives is grandfathered
+  // in (Barts besluit, 3-8): the rule is exact from that moment forward rather
+  // than retroactively suspicious of relationships that already exist.
+  {
+    const had = db.prepare("SELECT COUNT(*) AS n FROM pragma_table_info('ap_followers') WHERE name = 'gate_approved'").get();
+    ensureColumn('ap_followers', 'gate_approved', 'INTEGER DEFAULT 0');
+    if (!had || !had.n) {
+      try { db.prepare('UPDATE ap_followers SET gate_approved = 1').run(); } catch { /* table still empty on a fresh init */ }
+    }
+  }
   ensureColumn('posts', 'c2s_attachments', 'TEXT'); // media a C2S Note carried (JSON [{url,mediaType,name}]); buildNote federates them
   // 30-7: C2S posts briefly got their content media copied onto the cover,
Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 3d882bdbd7d45a727ffdcb64eacb870baecdb05d)
+++ src/routes/activitypub.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -254,4 +254,8 @@
 queueRoute('offers', (id, slug, me) => Guardianship.offersCollection(id, slug, me));
 queueRoute('follows', (id) => Guardianship.followsCollection(id));
+// §5.3 turned around (shaer-p729): what this ward has asked to follow, still
+// waiting on its guardians. Owner-only like the rest — who a child wants to
+// follow is nobody else's business.
+queueRoute('outgoing-follows', (id, slug, me) => Guardianship.outgoingFollowsCollection(id, slug, me));
 queueRoute('wards', (id, slug) => Guardianship.wardsCollection(id, slug));
 // Availability (FEP-633c 3.6.1) is never public: the ward reads its
@@ -696,5 +700,7 @@
   // 201 Created → Location header (AP spec); 202 Accepted for side-effect verbs.
   if (out.status === 201 && out.url) res.set('Location', out.url);
-  return res.status(out.status || 202).json({ ok: true, id: out.id, url: out.url });
+  // `state` carries a third outcome the app must be able to tell apart from a
+  // plain success: a ward's follow held for its guardians (§5.3, shaer-p729).
+  return res.status(out.status || 202).json({ ok: true, id: out.id, url: out.url, ...(out.state ? { state: out.state } : {}) });
 });
 
Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision 3d882bdbd7d45a727ffdcb64eacb870baecdb05d)
+++ src/routes/guardian.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -278,4 +278,31 @@
 });
 
+// ── §5.3, the other direction (shaer-p729): the ward wants to follow SOMEONE,
+//    and the guardians decide. Same quorum arithmetic and the same availability
+//    rules as the inbound gate above; only the question is turned around, which
+//    is why it gets its own endpoint rather than a flag on that one.
+router.post('/api/outgoing-follow/:id', requireAuth, express.json({ limit: '4kb' }), async (req, res) => {
+  const site = siteForUser(req);
+  if (!site) return res.status(404).json({ error: 'no_site' });
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  const me = AP.actorId(base, site.slug);
+  const decision = req.body?.decision === 'reject' ? 'reject' : 'approve';
+
+  const pending = Guardianship.outgoing.getPending(req.params.id);
+  if (!pending) return res.status(404).json({ error: 'gone' });
+  const allGuardians = Guardianship.listGuardians(pending.ward_slug).map((g) => g.other_uri);
+  if (!allGuardians.includes(me)) return res.status(403).json({ error: 'not_a_guardian' });
+  Guardianship.availability.oneAnswer(me, Date.now());
+  const guardians = Guardianship.availability.availableSet(pending.ward_slug, allGuardians, Date.now());
+  const r = Guardianship.outgoing.decide(pending.id, me, decision, guardians);
+  try {
+    // Only on approval does anything leave the building. A refusal is a local
+    // fact: the follow was never sent, so there is nothing out there to undo
+    // and nobody to inform that a child asked about them.
+    if (r.outcome === 'approved') await AP.performApprovedFollow(r.follow);
+  } catch { return res.status(502).json({ error: 'delivery', outcome: r.outcome }); }
+  res.json({ ok: true, outcome: r.outcome });
+});
+
 // ── Wave (FEP-633c §5, shaer:wave): a gentle "thinking of you" from a
 //    guardian to a ward. A private direct note, never a feed post. Warmth
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 3d882bdbd7d45a727ffdcb64eacb870baecdb05d)
+++ src/services/ActivityPubService.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -2487,4 +2487,15 @@
         const actorUri = c2sIdOf(object);
         if (!actorUri) return { status: 400, error: 'missing_object' };
+        // FEP-633c §5.3 outbound (shaer-p729): a ward asks its guardians first.
+        // A held request is a THIRD outcome — not sent, not failed — and it
+        // travels to the app as one, so Shaer can show "waiting" instead of a
+        // tile that already looks followed.
+        const held = await gateOutgoingFollow(site, actorUri);
+        if (held) {
+          return {
+            status: 202, url: actorUri, id: held.id,
+            state: held.status === 'denied' ? 'refused_by_guardian' : 'awaiting_guardian',
+          };
+        }
         // The error REACHES the app (Robins melding, 31-7): swallowing it
         // made a failed follow look exactly like a successful one.
@@ -4024,4 +4035,89 @@
 // Accept to the follower and record them, so delivery (incl. followers-only)
 // begins. `pending` is a row from ap_pending_follows.
+/**
+ * FEP-633c §5.3, the direction that was never gated (bead shaer-p729).
+ *
+ * A ward's OWN follow waited for nobody: it went straight out and the guardians
+ * got a note afterwards (1a2f206). That is informing, not gating — the door is
+ * already open when the message lands. Now it waits, with two exceptions that
+ * are not favours but the same decision already taken:
+ *
+ *   - the target is one of the ward's own guardians. Following the adult who
+ *     watches over you is not a question anyone needs to answer.
+ *   - the target already follows the ward THROUGH THE GATE. A guardian
+ *     approved that person by name; asking again about the same person only
+ *     teaches everyone to stop reading the question.
+ *
+ * Returns the held request, or null when the follow may go out now.
+ * Deliberately not a boolean: a held follow must be distinguishable from a sent
+ * one all the way up to the app, which is the lesson the error path already
+ * learned (Robins melding, 31-7).
+ */
+export async function gateOutgoingFollow(site, targetUri) {
+  const slug = site && site.slug;
+  if (!slug || !targetUri) return null;
+  const guardians = Guardianship.listGuardians(slug).map((g) => g.other_uri);
+  if (!guardians.length) return null;                                   // not a ward: nothing to gate
+  if (guardians.includes(targetUri)) return null;                       // your own guardian
+  if (Guardianship.outgoing.isMutual(slug, targetUri)) return null;     // already vetted by name
+
+  const seen = Guardianship.outgoing.findFor(slug, targetUri);
+  if (seen && seen.status === 'approved') return null;                  // the guardians said yes already
+  if (seen && (seen.status === 'pending' || seen.status === 'denied')) return seen;
+
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  const wardActor = actorId(base, slug);
+  const target = await fetchActor(targetUri).catch(() => null);
+  const ti = actorInfo(target, targetUri);
+  const id = `${wardActor}#outfollow-${Date.now()}-${rid()}`;
+  const held = Guardianship.outgoing.recordPending(slug, {
+    id, target: targetUri,
+    inbox: target && ((target.endpoints && target.endpoints.sharedInbox) || target.inbox),
+    name: ti.name, handle: ti.handle, icon: ti.icon,
+  });
+
+  // Same routing as the inbound gate: a guardian on this instance gets a push
+  // and reads /guardian; one elsewhere gets an Offer delivered so its own
+  // server holds a copy to answer from.
+  const wardKeys = getOrCreateKeys(slug);
+  const followObj = { id, type: 'Follow', actor: wardActor, object: targetUri };
+  for (const g of guardians) {
+    try { Guardianship.availability.recordRequest(slug, g, id, Date.now()); } catch { /* never load-bearing */ }
+  }
+  for (const g of guardians) {
+    const gslug = g.startsWith(`${base}/`) ? slugFromActorUrl(g) : null;
+    const isLocal = gslug && db.prepare('SELECT 1 FROM sites WHERE slug = ?').get(gslug);
+    if (isLocal) {
+      const L = pushLang(gslug);
+      pushEvent(gslug, { type: 'guardian', title: i18nT(L, 'push.n_guard_cog_t'), body: i18nT(L, 'push.n_guard_cog_b', { who: ti.name || ti.handle || i18nT(L, 'notif.someone') }), url: `${pushPrefix(gslug)}/guardian` });
+    } else {
+      fetchActor(g).then((ga) => {
+        const inbox = ga && ((ga.endpoints && ga.endpoints.sharedInbox) || ga.inbox);
+        if (!inbox) return;
+        const offer = { '@context': AP_CONTEXT, id: `${wardActor}#outfollowoffer-${Date.now()}-${rid()}`, type: 'Offer', actor: wardActor, to: [g], object: followObj, 'shaer:followApproval': true, 'shaer:direction': 'outgoing' };
+        deliverWithRetry(slug, inbox, offer, `${wardActor}#main-key`, wardKeys.private_pem).catch(() => {});
+      }).catch(() => {});
+    }
+  }
+  console.log('[AP] outgoing Follow', slug, '→', targetUri, '(gated, awaiting guardians)');
+  return held || { id, ward_slug: slug, target_uri: targetUri, status: 'pending' };
+}
+
+/**
+ * The guardians said yes: send the ward's Follow for real (§5.3, shaer-p729).
+ *
+ * The row stays behind as `approved` rather than being deleted. It is the
+ * record that these guardians vetted this target, so an unfollow-and-refollow
+ * later does not put the same question in front of them again.
+ */
+export async function performApprovedFollow(pending) {
+  const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(pending.ward_slug);
+  if (!site) return { error: 'no_such_ward' };
+  const r = await followActor(site, pending.target_uri);
+  if (r && r.error) return { error: r.error };
+  console.log('[AP] outgoing Follow approved', pending.ward_slug, '→', pending.target_uri);
+  return { ok: true };
+}
+
 export async function acceptGatedFollow(pending) {
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
@@ -4030,4 +4126,8 @@
   const keys = getOrCreateKeys(slug);
   fStmts().ins.run(slug, pending.follower_uri, pending.follower_inbox, pending.follower_shared_inbox, pending.follower_name, pending.follower_handle, pending.follower_icon);
+  // This follower came through the §5.3 gate: a guardian said yes to this
+  // person by name. That is precisely what lets the ward follow them back later
+  // without asking the same guardians the same question twice (shaer-p729).
+  db.prepare('UPDATE ap_followers SET gate_approved = 1 WHERE slug = ? AND actor_uri = ?').run(slug, pending.follower_uri);
   const original = pending.activity_json ? JSON.parse(pending.activity_json) : { type: 'Follow', actor: pending.follower_uri, object: me };
   const accept = { '@context': AP_CONTEXT, id: `${me}#accept-${Date.now()}-${rid()}`, type: 'Accept', actor: me, object: original };
@@ -4512,4 +4612,5 @@
   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, handleMoveInbox, moveAccount, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
   acceptGatedFollow, rejectGatedFollow, isWardGuardian, outboxAudience, sendFollowDecision,
+  gateOutgoingFollow, performApprovedFollow,
   parseOwnPoll, pollTally, ownPollView, deliverPollUpdate, maybeCrawlThread, sendReport, localMentionSlugs,
   autoBoostCount, boostedCount, markBoosted, unmarkBoosted, markLiked, unmarkLiked, getTimelineReaction, upsertBoostedNote, getCirkelPosts, getCirkelMembers, selfHealTimeline,
Index: src/services/guardianship/index.js
===================================================================
--- src/services/guardianship/index.js	(revision 3d882bdbd7d45a727ffdcb64eacb870baecdb05d)
+++ src/services/guardianship/index.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -19,8 +19,9 @@
 export { wireDelivery, c2sVisibility, deliverDirectNote } from './delivery.js';
 export { wireHandshake, handleOutbox as handleGuardianshipOutbox, handleInbox as handleGuardianshipInbox, parseRelationship, parseUndoRelationship, endGuardianship } from './handshake.js';
-export { offersCollection, followsCollection, wardsCollection, guardiansCollection } from './queues.js';
+export { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection } from './queues.js';
 export * as availability from './availability.js';
 export { wireAvailability } from './availability.js';
 export * as follows from './follows.js';
+export * as outgoing from './outgoing.js';
 export { listForParty as listOffersForParty, getOffer, findOfferAnywhere } from './offers.js';
 export {
Index: src/services/guardianship/outgoing.js
===================================================================
--- src/services/guardianship/outgoing.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
+++ src/services/guardianship/outgoing.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -0,0 +1,117 @@
+/**
+ * Guardianship (FEP-633c §5.3, the other direction) — gating a ward's OWN
+ * follows. Bead shaer-p729; the design is in docs/ward-outbound-follows-design.md,
+ * and the spec question it answers is shaer-yeo5.
+ *
+ * The inbound gate in `follows.js` decides who may follow a ward. This one
+ * decides who a ward may follow. Until now that went out unchecked: the
+ * guardians got a note afterwards (1a2f206), which is informing, not gating —
+ * the door is already open by the time the message arrives.
+ *
+ * The rule (Barts besluit): every outgoing follow waits for a guardian, EXCEPT
+ * where the target already follows the ward through the gate. A guardian
+ * already said yes to that person; asking the same question twice only teaches
+ * people to stop reading the question.
+ */
+import db from '../../config/database.js';
+
+let _s = null;
+function stmts() {
+  if (!_s) {
+    _s = {
+      ins: db.prepare(`INSERT OR IGNORE INTO ap_pending_outgoing_follows
+        (id, ward_slug, target_uri, target_inbox, target_name, target_handle, target_icon, quorum, created_at)
+        VALUES (?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP)`),
+      get: db.prepare('SELECT * FROM ap_pending_outgoing_follows WHERE id = ?'),
+      byTarget: db.prepare('SELECT * FROM ap_pending_outgoing_follows WHERE ward_slug = ? AND target_uri = ?'),
+      byWard: db.prepare("SELECT * FROM ap_pending_outgoing_follows WHERE ward_slug = ? AND status = 'pending' ORDER BY created_at DESC"),
+      approve: db.prepare('INSERT OR IGNORE INTO ap_outgoing_follow_approvals (follow_id, guardian_uri, decision, created_at) VALUES (?,?,?,CURRENT_TIMESTAMP)'),
+      answers: db.prepare('SELECT guardian_uri, decision FROM ap_outgoing_follow_approvals WHERE follow_id = ?'),
+      setStatus: db.prepare('UPDATE ap_pending_outgoing_follows SET status = ? WHERE id = ?'),
+      del: db.prepare('DELETE FROM ap_pending_outgoing_follows WHERE id = ?'),
+      delByTarget: db.prepare('DELETE FROM ap_pending_outgoing_follows WHERE ward_slug = ? AND target_uri = ?'),
+      gateApproved: db.prepare('SELECT 1 FROM ap_followers WHERE slug = ? AND actor_uri = ? AND gate_approved = 1'),
+    };
+  }
+  return _s;
+}
+
+/**
+ * Does this target already follow the ward, with a guardian's blessing?
+ *
+ * Only a gate-approved follower counts. A follower a free actor picked up
+ * before it was ever a ward was never seen by a guardian, so following them
+ * back is a new question, not a settled one. (Rows that predate the marker are
+ * grandfathered at migration; see config/database.js.)
+ */
+export function isMutual(wardSlug, targetUri) {
+  return !!stmts().gateApproved.get(wardSlug, targetUri);
+}
+
+/** Record an outgoing follow awaiting guardian approval. */
+export function recordPending(wardSlug, f) {
+  stmts().ins.run(
+    f.id, wardSlug, f.target, f.inbox || null,
+    f.name || null, f.handle || null, f.icon || null, f.quorum || 'any',
+  );
+  return stmts().byTarget.get(wardSlug, f.target);
+}
+
+export function getPending(id) { return stmts().get.get(id); }
+export function findFor(wardSlug, targetUri) { return stmts().byTarget.get(wardSlug, targetUri); }
+
+/** Outgoing follows this ward is waiting on — the guardian's queue. */
+export function listForWard(wardSlug) { return stmts().byWard.all(wardSlug); }
+
+/**
+ * A guardian's answer. Same shape and the same quorum arithmetic as the
+ * inbound gate, so the two directions cannot drift apart in how they count:
+ * a single reject denies outright, approvals accumulate toward the quorum.
+ */
+export function decide(id, guardianUri, decision, guardiansOfWard) {
+  const follow = stmts().get.get(id);
+  if (!follow || follow.status !== 'pending') return { outcome: 'gone', follow };
+  stmts().approve.run(id, guardianUri, decision === 'reject' ? 'reject' : 'approve');
+  const rows = stmts().answers.all(id);
+  if (rows.some((r) => r.decision === 'reject')) {
+    stmts().setStatus.run('denied', id);
+    return { outcome: 'rejected', follow };
+  }
+  const approvers = new Set(rows.filter((r) => r.decision === 'approve').map((r) => r.guardian_uri));
+  const guardians = (guardiansOfWard || []).filter(Boolean);
+  const enough = follow.quorum === 'all'
+    ? guardians.length > 0 && guardians.every((g) => approvers.has(g))
+    : approvers.size >= 1;                                   // 'any' (default)
+  if (enough) {
+    stmts().setStatus.run('approved', id);
+    return { outcome: 'approved', follow };
+  }
+  return { outcome: 'waiting', follow };
+}
+
+/** The ward changed its mind, or blocked the target: the request is gone. */
+export function withdraw(wardSlug, targetUri) { stmts().delByTarget.run(wardSlug, targetUri); }
+export function remove(id) { stmts().del.run(id); }
+
+/** One request as the queue item the Shaer clients parse, mirroring the
+ *  inbound gated-follow item so a dashboard can render both side by side. */
+export function queueItem(o, me) {
+  const rows = stmts().answers.all(o.id);
+  return {
+    id: o.id,
+    type: 'Follow',
+    actor: o.ward_slug,
+    object: o.target_uri,
+    'shaer:direction': 'outgoing',
+    'shaer:target': o.target_uri,
+    'shaer:targetHandle': o.target_handle || undefined,
+    'shaer:quorum': o.quorum || 'any',
+    'shaer:approvals': rows.filter((r) => r.decision === 'approve').length,
+    'shaer:myVote': rows.some((r) => r.guardian_uri === me),
+    published: o.created_at,
+  };
+}
+
+export default {
+  isMutual, recordPending, getPending, findFor, listForWard, decide, withdraw, remove, queueItem,
+};
Index: src/services/guardianship/queues.js
===================================================================
--- src/services/guardianship/queues.js	(revision 3d882bdbd7d45a727ffdcb64eacb870baecdb05d)
+++ src/services/guardianship/queues.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -12,4 +12,5 @@
 import * as relations from './relations.js';
 import * as availability from './availability.js';
+import * as outgoing from './outgoing.js';
 import * as handshake from './handshake.js';
 
@@ -39,4 +40,9 @@
 }
 
+/** §5.3 outbound: this ward's own follow requests, waiting for its guardians. */
+export function outgoingFollowsCollection(id, slug, me) {
+  return collection(id, outgoing.listForWard(slug).map((o) => outgoing.queueItem(o, me)));
+}
+
 /** The guardian's committed wards, with cached handle for display. */
 export function wardsCollection(id, slug) {
@@ -53,3 +59,3 @@
 }
 
-export default { offersCollection, followsCollection, wardsCollection, guardiansCollection };
+export default { offersCollection, followsCollection, outgoingFollowsCollection, wardsCollection, guardiansCollection };
Index: src/services/guardianship/relations.js
===================================================================
--- src/services/guardianship/relations.js	(revision 3d882bdbd7d45a727ffdcb64eacb870baecdb05d)
+++ src/services/guardianship/relations.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -73,4 +73,9 @@
       offers: `${id}/queues/offers`,
       follows: `${id}/queues/follows`,
+      // Both directions of §5.3, kept apart on purpose: a guardian must be able
+      // to tell "someone wants to follow your ward" from "your ward wants to
+      // follow someone". Same mechanism, opposite question, different words in
+      // the interface (shaer-p729).
+      outgoingFollows: `${id}/queues/outgoing-follows`,
       wards: `${id}/queues/wards`,
       guardians: `${id}/queues/guardians`,
Index: test/activitypub-as2.test.js
===================================================================
--- test/activitypub-as2.test.js	(revision 3d882bdbd7d45a727ffdcb64eacb870baecdb05d)
+++ test/activitypub-as2.test.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -35,5 +35,7 @@
   // sub-keys are the daemon-contract collection names the Shaer clients read.
   // `guardians` is the availability queue (3.6.1: never public, owner-only).
-  'shaer:queues', 'offers', 'follows', 'wards', 'guardians',
+  // `outgoingFollows` is §5.3 turned around: the ward's own follow requests,
+  // waiting for the guardians (shaer-p729).
+  'shaer:queues', 'offers', 'follows', 'outgoingFollows', 'wards', 'guardians',
   // ActivityPub §4.1 `endpoints` vocabulary (same category as sharedInbox), used for C2S.
   'oauthAuthorizationEndpoint', 'oauthTokenEndpoint', 'uploadMedia',
Index: test/outgoing-follow-gate.test.js
===================================================================
--- test/outgoing-follow-gate.test.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
+++ test/outgoing-follow-gate.test.js	(revision fa33214eb34752fafee25fc95942f470701006f8)
@@ -0,0 +1,111 @@
+// FEP-633c §5.3, the direction that was never gated (bead shaer-p729).
+//
+// A ward's own follow used to go straight out; the guardians got a note
+// afterwards, which is informing, not gating — the door is already open by the
+// time the message lands. Now it waits, with two exceptions that are not
+// favours but the same decision already taken: the ward's own guardian, and
+// someone a guardian already admitted through the inbound gate.
+//
+// The mutual shortcut only counts followers who came through that gate. A
+// follower a free actor collected before it was ever a ward was never seen by
+// a guardian, so following them back is a new question. Rows that predate the
+// marker are grandfathered (Barts besluit, 3-8).
+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;
+const G = await import('../src/services/guardianship/index.js');
+
+const BASE = 'https://test.example';
+const local = (slug) => `${BASE}/ap/users/${slug}`;
+const STRANGER = 'https://elders.example/users/stranger';
+const PAL = 'https://elders.example/users/pal';
+const OLDPAL = 'https://elders.example/users/oldpal';
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'u1', 'u1@test', 'x', 'god');
+let n = 0;
+function site(slug) {
+  db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)')
+    .run(`s${++n}`, slug, slug, 'u1', n === 1 ? 1 : 0);
+  return db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug);
+}
+const guards = (slug, other) =>
+  db.prepare("INSERT INTO ap_guardianships (slug, role, other_uri, status) VALUES (?, 'ward', ?, 'accepted')")
+    .run(slug, other);
+const follower = (slug, uri, gateApproved) =>
+  db.prepare('INSERT INTO ap_followers (slug, actor_uri, inbox, gate_approved) VALUES (?,?,?,?)')
+    .run(slug, uri, `${uri}/inbox`, gateApproved ? 1 : 0);
+
+const kid = site('kid');
+site('mum');
+guards('kid', local('mum'));          // kid is a ward, watched by mum
+follower('kid', PAL, true);           // came through the §5.3 gate
+follower('kid', OLDPAL, false);       // followed back when kid was still free
+
+const free = site('freebird');        // no guardians at all
+
+test('a free actor is not gated at all', async () => {
+  assert.equal(await AP.gateOutgoingFollow(free, STRANGER), null,
+    'guardianship is the only thing that gates a follow; a free account keeps its own counsel');
+});
+
+test('a stranger has to wait for the guardians', async () => {
+  const held = await AP.gateOutgoingFollow(kid, STRANGER);
+  assert.ok(held, 'held, not sent');
+  assert.equal(held.status, 'pending');
+  assert.equal(held.target_uri, STRANGER);
+});
+
+test('following your own guardian needs nobody\'s permission', async () => {
+  assert.equal(await AP.gateOutgoingFollow(kid, local('mum')), null,
+    'asking mum whether you may follow mum is not a question');
+});
+
+test('a follower the guardians already admitted may be followed back', async () => {
+  assert.equal(await AP.gateOutgoingFollow(kid, PAL), null,
+    'a guardian said yes to this person by name; asking twice teaches people to stop reading');
+});
+
+test('but a follower from before the guardians existed is a fresh question', async () => {
+  const held = await AP.gateOutgoingFollow(kid, OLDPAL);
+  assert.ok(held, 'never went through the gate, so nobody ever vetted them');
+  assert.equal(held.status, 'pending');
+});
+
+test('asking twice does not queue the same request twice', async () => {
+  const again = await AP.gateOutgoingFollow(kid, STRANGER);
+  assert.ok(again);
+  assert.equal(G.outgoing.listForWard('kid').filter((o) => o.target_uri === STRANGER).length, 1);
+});
+
+test('the guardians see it in their own queue, apart from the inbound one', () => {
+  const q = G.outgoingFollowsCollection(`${local('kid')}/queues/outgoing-follows`, 'kid', local('mum'));
+  const mine = q.orderedItems.filter((o) => o['shaer:target'] === STRANGER);
+  assert.equal(mine.length, 1);
+  assert.equal(mine[0]['shaer:direction'], 'outgoing',
+    'a guardian must be able to tell "wants to follow your ward" from "your ward wants to follow"');
+  assert.equal(mine[0]['shaer:myVote'], false);
+});
+
+test('a guardian approving lets it through from then on', async () => {
+  const pending = G.outgoing.listForWard('kid').find((o) => o.target_uri === STRANGER);
+  const r = G.outgoing.decide(pending.id, local('mum'), 'approve', [local('mum')]);
+  assert.equal(r.outcome, 'approved');
+  assert.equal(await AP.gateOutgoingFollow(kid, STRANGER), null,
+    'the row stays behind as the record, so an unfollow and refollow is not a second question');
+});
+
+test('a refusal is remembered too, and does not re-ask by re-tapping', async () => {
+  const held = await AP.gateOutgoingFollow(kid, OLDPAL);
+  const r = G.outgoing.decide(held.id, local('mum'), 'reject', [local('mum')]);
+  assert.equal(r.outcome, 'rejected');
+  const again = await AP.gateOutgoingFollow(kid, OLDPAL);
+  assert.equal(again.status, 'denied', 'tapping follow again does not put it back in front of mum');
+});
