diff --git a/keyserver/src/responders/farcaster-webhook-responders.js b/keyserver/src/responders/farcaster-webhook-responders.js
--- a/keyserver/src/responders/farcaster-webhook-responders.js
+++ b/keyserver/src/responders/farcaster-webhook-responders.js
@@ -2,10 +2,13 @@
 
 import { createHmac } from 'crypto';
 import type { $Request } from 'express';
+import invariant from 'invariant';
 
 import bots from 'lib/facts/bots.js';
 import { extractKeyserverIDFromID } from 'lib/keyserver-conn/keyserver-call-utils.js';
+import { createSidebarThreadName } from 'lib/shared/sidebar-utils.js';
 import { type NeynarWebhookCastCreatedEvent } from 'lib/types/farcaster-types.js';
+import { messageTypes } from 'lib/types/message-types-enum.js';
 import { threadTypes } from 'lib/types/thread-types-enum.js';
 import { type NewThreadResponse } from 'lib/types/thread-types.js';
 import { neynarWebhookCastCreatedEventValidator } from 'lib/types/validators/farcaster-webhook-validators.js';
@@ -17,10 +20,16 @@
   createOrUpdateFarcasterChannelTag,
   farcasterChannelTagBlobValidator,
 } from '../creators/farcaster-channel-tag-creator.js';
+import createMessages from '../creators/message-creator.js';
 import { createThread } from '../creators/thread-creator.js';
 import { fetchServerThreadInfos } from '../fetchers/thread-fetchers.js';
 import { createBotViewer } from '../session/bots.js';
-import { updateRole } from '../updaters/thread-updaters.js';
+import { createScriptViewer } from '../session/scripts.js';
+import {
+  changeRole,
+  commitMembershipChangeset,
+} from '../updaters/thread-permission-updaters.js';
+import { updateRole, joinThread } from '../updaters/thread-updaters.js';
 import { thisKeyserverAdmin, thisKeyserverID } from '../user/identity.js';
 import { getFarcasterBotConfig } from '../utils/farcaster-bot.js';
 import { getVerifiedUserIDForFID } from '../utils/farcaster-utils.js';
@@ -34,6 +43,70 @@
 const { commbot } = bots;
 const commbotViewer = createBotViewer(commbot.userID);
 
+async function createCastSidebar(
+  sidebarCastHash: string,
+  channelName: ?string,
+  channelCommunityID: string,
+  taggerUserID: ?string,
+): Promise<?NewThreadResponse> {
+  const sidebarCast =
+    await neynarClient?.fetchFarcasterCastByHash(sidebarCastHash);
+
+  if (!sidebarCast) {
+    return null;
+  }
+
+  const castAuthor = sidebarCast.author.username;
+  const channelText = channelName ? ` in channel ${channelName}` : '';
+  const messageText = `${castAuthor} said "${sidebarCast.text}"${channelText}`;
+
+  let viewer = commbotViewer;
+  if (taggerUserID) {
+    viewer = createScriptViewer(taggerUserID);
+    await joinThread(viewer, {
+      threadID: channelCommunityID,
+    });
+  } else {
+    const changeset = await changeRole(
+      channelCommunityID,
+      [commbot.userID],
+      -1,
+    );
+    await commitMembershipChangeset(viewer, changeset);
+  }
+
+  const [{ id: messageID }] = await createMessages(viewer, [
+    {
+      type: messageTypes.TEXT,
+      threadID: channelCommunityID,
+      creatorID: viewer.id,
+      time: Date.now(),
+      text: messageText,
+    },
+  ]);
+
+  invariant(
+    messageID,
+    'message returned from createMessages always has ID set',
+  );
+  const sidebarThreadName = createSidebarThreadName(messageText);
+
+  const response = await createThread(
+    viewer,
+    {
+      type: threadTypes.SIDEBAR,
+      parentThreadID: channelCommunityID,
+      name: sidebarThreadName,
+      sourceMessageID: messageID,
+    },
+    {
+      addViewerAsGhost: !taggerUserID,
+    },
+  );
+
+  return response;
+}
+
 async function createTaggedFarcasterCommunity(
   channelID: string,
   taggerUserID: ?string,
@@ -198,7 +271,24 @@
       throw new ServerError('blob_fetch_failed');
     }
   }
-  console.log(channelCommunityID);
+
+  const { hash: castHash, parent_hash: parentHash } = event.data;
+  const taggerUserID = await taggerUserIDPromise;
+
+  // we use the parent cast to create the sidebar source message if it exists
+  const sidebarCastHash = parentHash ?? castHash;
+  const sidebarThreadResponse = await createCastSidebar(
+    sidebarCastHash,
+    eventChannel?.name,
+    channelCommunityID,
+    taggerUserID,
+  );
+
+  if (!sidebarThreadResponse) {
+    return;
+  }
+
+  console.log(sidebarThreadResponse);
 }
 
 export { taggedCommFarcasterResponder, taggedCommFarcasterInputValidator };
diff --git a/lib/shared/sidebar-utils.js b/lib/shared/sidebar-utils.js
--- a/lib/shared/sidebar-utils.js
+++ b/lib/shared/sidebar-utils.js
@@ -53,6 +53,10 @@
   ...
 };
 
+function createSidebarThreadName(messageTitle: string): string {
+  return trimText(messageTitle, 30);
+}
+
 function baseCreatePendingSidebar(
   input: BaseCreatePendingSidebarInput,
 ): ThreadInfo {
@@ -63,7 +67,7 @@
     messageTitle,
   } = input;
   const { color, type: parentThreadType } = parentThreadInfo;
-  const threadName = trimText(messageTitle, 30);
+  const threadName = createSidebarThreadName(messageTitle);
 
   const initialMembers = new Map<string, UserIDAndUsername>();
 
@@ -246,4 +250,5 @@
   createPendingSidebar,
   useCanCreateSidebarFromMessage,
   useSidebarExistsOrCanBeCreated,
+  createSidebarThreadName,
 };