Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision 329873ef53dfe145c0b016df53555fa8de5db70c)
+++ src/routes/guardian.js	(revision c8e03c6553401711c2e513b26dd53e24cc5a9486)
@@ -516,7 +516,14 @@
     // the proposal exists and a threshold of two can never be met.
     if (r.state === 'open') {
+      const wardActor = AP.actorId(base, localWard.slug);
       for (const g of Guardianship.listGuardians(localWard.slug).map((x) => x.other_uri)) {
         if (g === me) continue;
-        AP.deliverToActor(site, g, { ...offer, to: [g] }).catch(() => { /* queued */ });
+        // Signed by the ward, so the body must say the ward: anything else is
+        // a signer mismatch and the receiver answers 401 (as it should).
+        AP.deliverToActor(
+          db.prepare('SELECT * FROM sites WHERE slug = ?').get(localWard.slug),
+          g,
+          { ...offer, actor: wardActor, to: [g], 'shaer:proposer': me },
+        ).catch(() => { /* queued */ });
       }
     }
Index: src/services/guardianship/handshake.js
===================================================================
--- src/services/guardianship/handshake.js	(revision 329873ef53dfe145c0b016df53555fa8de5db70c)
+++ src/services/guardianship/handshake.js	(revision c8e03c6553401711c2e513b26dd53e24cc5a9486)
@@ -317,6 +317,13 @@
           for (const g of relations.listGuardians(site.slug).map((x) => x.other_uri)) {
             if (g === actor) continue;   // the proposer already answered
+            // The forward goes out AS THE WARD, because the ward's key signs
+            // it. Keeping the proposer in `actor` made every receiver answer
+            // 401 signer mismatch, and rightly so: the body claimed one author
+            // and the signature proved another. §5.3 forwards a gated follow
+            // the same way. Who proposed it rides along separately, for the
+            // guardian's screen.
             deps.deliverTo(site, g, {
-              id: offerId, type: 'Offer', actor, to: [g], object: activity.object,
+              id: offerId, type: 'Offer', actor: me, to: [g], object: activity.object,
+              'shaer:proposer': actor,
             }).catch(() => { /* the delivery queue retries */ });
           }
@@ -333,5 +340,8 @@
         gated.recordGatedReview(site.slug, {
           id: offerId, wardUri: gs.ward, wardInbox: wardDoc && wardDoc.inbox,
-          proposer: actor, feature: gs.feature, value: gs.value,
+          // A forward is signed by the ward, so `actor` is the ward; the
+          // guardian who opened it travels in shaer:proposer.
+          proposer: (typeof activity['shaer:proposer'] === 'string' ? activity['shaer:proposer'] : actor),
+          feature: gs.feature, value: gs.value,
         });
         notify(site.slug, { kind: 'gated_review', feature: gs.feature, value: gs.value, ward: gs.ward });
Index: test/gated-settings.test.js
===================================================================
--- test/gated-settings.test.js	(revision 329873ef53dfe145c0b016df53555fa8de5db70c)
+++ test/gated-settings.test.js	(revision c8e03c6553401711c2e513b26dd53e24cc5a9486)
@@ -116,6 +116,16 @@
 
   // The forward: B and C are told, A is not asked twice.
-  const told = sent.filter((x) => x.act.object && x.act.object['shaer:feature']).map((x) => x.to).sort();
-  assert.deepEqual(told, [B, C].sort(), 'both other guardians must receive the proposal');
+  const fwd = sent.filter((x) => x.act.object && x.act.object['shaer:feature']);
+  assert.deepEqual(fwd.map((x) => x.to).sort(), [B, C].sort(), 'both other guardians must receive the proposal');
+  // And it must go out AS THE WARD, because the ward's key signs it. Sending
+  // it with the proposer still in `actor` is a signer mismatch: every receiver
+  // answers 401 and the proposal silently never arrives. Live proof, from
+  // beta's log: "guardianship Offer got 401 from boiert.eu/.../inbox". The
+  // first version of this test checked THAT a forward happened and not on
+  // whose behalf, so it passed while nothing worked.
+  for (const x of fwd) {
+    assert.equal(x.act.actor, WARD, 'the forward is signed by the ward, so it must say the ward');
+    assert.equal(x.act['shaer:proposer'], A, 'and it carries who actually proposed it');
+  }
 
   // B receives its copy on its own server and can answer it.
@@ -123,7 +133,11 @@
   database.prepare("INSERT OR IGNORE INTO ap_guardianships (slug, role, other_uri, status, offer_id) VALUES ('gb','guardian',?, 'accepted','o')").run(WARD);
   const gbSite = database.prepare('SELECT * FROM sites WHERE slug = ?').get('gb');
-  assert.equal(await G.handleGuardianshipInbox(gbSite, { ...offer, actor: A, to: ['https://test.example/ap/users/gb'] }), true);
+  // Exactly the shape the ward sends: actor = the ward, proposer alongside.
+  assert.equal(await G.handleGuardianshipInbox(gbSite, {
+    ...offer, actor: WARD, 'shaer:proposer': A, to: ['https://test.example/ap/users/gb'],
+  }), true);
   const review = G.gated.listGatedReviews('gb')[0];
   assert.ok(review, 'the guardian stores a copy it can answer');
+  assert.equal(review.proposer, A, 'the screen names who proposed it, not the ward that relayed it');
   assert.equal(review.feature, 'shaer:externalEmbeds');
   assert.equal(review.ward_uri, WARD);
