{"ok":true,"version":"agent-community-chat.v1","generatedAt":"2026-05-30T05:20:24.710Z","surface":{"id":"community-chat","artistId":"laurel","safeForAgent":true,"browserAutomationRequired":false,"relatedIssues":[507,528,573,616,622,629,639,640,648,988,1545,1546,1499,1500,1502,1503,1504,1539]},"principal":{"source":"guest","role":"guest","signedIn":false,"displayName":"Guest listener","userId":null,"email":null,"adminRole":null,"capabilities":{"readVisibleMessages":false,"postCommunityMessage":false,"reactToCommunityMessage":false,"moderateCommunityChat":false,"manageCommunityChannels":false,"proposeCreatorAutomation":false,"manageNotificationRouting":false}},"community":{"settings":{"artistId":"laurel","enabled":false,"channelsEnabled":false,"autoChannelsUnlockedAt":null,"maxChannels":4,"slowModeSeconds":0,"updatedAt":"2026-05-14T15:57:15.000Z"},"channels":[],"activeChannelId":null,"messages":[],"totalMessages":18,"channelsUnlocked":false,"displayWindowDays":14},"channelModel":{"version":"community-channel-model.v1","relatedIssue":921,"maxActiveChannels":4,"defaultChannel":{"id":"laurel-community-general","slug":"general","name":"General","countsTowardCap":true,"availability":"Every logged-in Laurel listener can use #general by default."},"terminology":{"alwaysOnCommunityChannels":"Persistent 24/7 listener text channels for the creator community, such as #general, #intros, #members, or an artist-named channel.","liveRoomNotes":"Event-scoped live-room notes tied to one scheduled, live, or replay room and stored separately in live_chat_messages.","eventLinkedCommunityChannel":"A live room can link to one always-on community channel so pre-show, live, replay, and between-stream conversation stays in the same thread."},"liveChatBoundary":{"communityMessageStore":"community_chat_messages","liveRoomNoteStore":"live_chat_messages","guidance":"Do not describe always-on community channels as per-stream chat; do not store stream-specific production notes in the community channel thread."},"accessPolicy":{"current":"Every channel returns `access.version=community-channel-access.v1`; message reads, writes, socket tokens, and notification joins use the same server-side access rule.","modes":["all-logged-in-listeners","membership-tiers","entitlement-groups"],"defaultChannel":"#general stays in all-logged-in-listeners mode so every signed-in Laurel listener can use it by default.","futureEntitlementGroups":"Entitlement-group rules are represented for forward compatibility, but no listener is granted by those groups until the entitlement resolver ships."}},"messageSemantics":{"version":"community-chat-affordances.v1","relatedIssues":[629,639,640,648,1500],"visibility":{"displayWindowDays":14,"publicStatuses":["visible"],"hiddenFlaggedBoundary":"Guest, listener, and ordinary agent reads return visible messages only. Hidden and flagged bodies stay behind the admin moderation surface."},"replies":{"parentField":"parentId","contextField":"replyTo","postInput":"replyToMessageId","parentRequirement":"Reply targets must be visible Laurel messages in the same active community channel; unavailable targets are rejected."},"reactions":{"supported":["heart","👍","👏","🔥","😂","✨"],"aggregateField":"reactionCounts","viewerField":"viewerReactions","toggleEndpoint":{"href":"https://fanful.net/api/community/messages/reactions","method":"POST","auth":"signed-in-listener","description":"Toggle the signed-in listener's reaction on a visible community message."},"record":"community_chat_message_reactions","permission":"signed-in listener only; automation tokens cannot react as listeners"},"mentions":{"field":"mentions","source":"Parsed from visible message bodies using scoped display labels.","notificationRouting":"Delivery is delegated to community-chat-notifications.v1 from #640, including membership, mute, Comments category, and email/push readiness gates.","boundary":"Mention parsing does not expose a private member directory; labels come from visible channel/reply context."},"automationReplies":{"authorField":"authorKind","authorValue":"automation","ruleField":"automationRuleId","triggerField":"automationTriggerMessageId","processor":"community-chat-automation-replies.v1","attribution":"Automated replies use the rule attribution label, render as creator-approved automation, and are not ordinary listener messages.","auditRecord":"community_chat_admin_actions.action=automation_reply_emit"}},"discordSlackParity":{"version":"community-chat-discord-slack-parity.v1","relatedIssues":[1499,1500,1502,1503,1504,1539],"readiness":{"typingPresence":"ready","systemEmojiReactions":"ready","customEmojiUpload":"ready","customEmojiReactions":"ready","listenerNotificationCenter":"ready","publisherNotificationCenter":"ready"},"typingPresence":{"socketClientMessage":{"type":"typing","typing":"boolean"},"socketServerMessage":"chat-typing","ttlMs":6000,"scope":"artist/community channel","participantFields":["viewerId","displayName","expiresAt"],"record":null,"boundary":"Typing presence is ephemeral Durable Object state; clients should render other participants only and stop showing stale names after expiresAt."},"systemEmojiReactions":{"supported":["heart","👍","👏","🔥","😂","✨"],"toggleEndpoint":{"href":"https://fanful.net/api/community/messages/reactions","method":"POST","auth":"signed-in-listener","description":"Toggle a supported system emoji on a visible community message."},"record":"community_chat_message_reactions","aggregateField":"reactionCounts","viewerField":"viewerReactions"},"customEmojis":{"record":"community_chat_custom_emojis","reactionRecordExtension":"community_chat_message_reactions.custom_emoji_id","storageBinding":"MEDIA","storageKeyPrefix":"community-emojis/{artistId}/{emojiId}","acceptedContentTypes":["image/png","image/jpeg","image/gif","image/webp"],"maxBytes":262144,"maxNameLength":32,"namePattern":"^[a-z0-9][a-z0-9_-]{1,31}$","communityScope":"artist_id is the V1 creator-community scope until multi-community ids ship.","uploaderOwnership":"uploader_user_id lets My emojis show emojis uploaded by the signed-in user even when they are reused in other future communities.","usageStats":"Usage totals and top 5 users are derived from community_chat_message_reactions filtered by custom_emoji_id and community message artist scope.","operations":{"list":"community.custom-emojis.list","upload":"community.custom-emojis.upload","remove":"community.custom-emojis.remove","react":"community.messages.react-custom-emoji"}},"notificationCenters":{"record":"community_notification_events","recipientRoles":["listener","publisher"],"eventTypes":["chat_reaction","chat_reply","live_scheduled","donation_thanks","moderation_review","creator_action"],"statusValues":["unread","read","archived"],"listenerEvents":["chat_reaction","chat_reply","live_scheduled","donation_thanks"],"publisherEvents":["moderation_review","creator_action","live_scheduled"],"deepLinkFields":["actionHref","channelId","messageId","liveEventId","customEmojiId"],"operations":{"listenerRead":"community.notifications.read","listenerMark":"community.notifications.mark","publisherRead":"community.publisher-notifications.read","publisherMark":"community.publisher-notifications.mark"},"boundary":"Notification events are in-app center records; email/push delivery remains governed by listener notification preferences and device registration."}},"eventLinkedChannels":[{"event":{"id":"laurel-studio-room","title":"Studio room","status":"ended","startsAt":"2026-08-08T00:00:00.000Z","endsAt":null,"communityChannelId":"laurel-community-general"},"channel":{"id":"laurel-community-general","slug":"general","name":"General","description":"Between-stream conversation for Laurel listeners.","status":"active","isDefault":true},"relationship":{"kind":"persistent-event-community-channel","communityMessageStore":"community_chat_messages","liveRoomNoteStore":"live_chat_messages","guidance":"Use the community channel for conversation that should persist before, during, and after the broadcast. Keep event-scoped production notes in the live-room note surface."}}],"workflowTriggers":{"communityMessageEscalated":{"type":"community.message.escalated","version":"agent-workflow-trigger.community-escalation.v1","status":"manifest-ready","delivery":"not-enabled","summary":"Community moderation changes record deterministic signed trigger status for authorized creator/operator agents. The subscription registry and manual test delivery are ready, but automatic runtime fan-out remains disabled.","recentEscalations":[],"idempotency":"Event id, delivery id, and idempotency key are deterministic from the community message id and audit timestamp, so duplicate delivery retries collapse to the same keys.","boundaries":["Creator/operator clients only see redacted trigger status when they already have community moderation capability.","Trigger payloads redact raw message bodies, raw user ids, Better Auth user ids, moderation notes, admin tokens, and agent bearer tokens.","Triggers may prepare moderation summaries or review queues, but public replies, hide/restore/delete decisions, and creator-speech actions still require confirmed action contracts."]}},"operations":[{"id":"community.channels.read","toolName":"community_channels_read","status":"ready","enabled":false,"summary":"Read Laurel community channels, active channel selection, and recent visible messages.","endpoint":{"href":"https://fanful.net/api/agent/community-chat","method":"GET","auth":"optional-session","description":"Read the agent-safe community chat contract and public visible message window.","query":["channelId"]},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":false,"currentRecord":null,"attribution":"Read-only access returns public visible channel data."},"input":{"channelId":{"type":"string","required":false,"description":"Community channel id to select; defaults to Laurel's active/default channel."}},"boundaries":["Returns visible community messages only.","Does not expose raw socket tokens, secrets, private invite links, or hidden moderation history."]},{"id":"community.messages.read","toolName":"fanful_listener_community_messages_read","status":"ready","enabled":false,"summary":"Read visible community messages from the selected channel.","endpoint":{"href":"https://fanful.net/api/agent/community-chat","method":"GET","auth":"signed-in-listener","description":"Read visible community channel messages through the agent-safe manifest after the shared channel access rule passes.","query":["channelId"]},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":false,"currentRecord":null,"attribution":"Read-only public channel access."},"input":{"channelId":{"type":"string","required":true,"description":"Community channel id."}},"boundaries":["Message reads require a signed-in listener or admin-capable principal that passes the channel access rule.","Messages include parentId, replyTo, reactionCounts, caller-scoped viewerReactions, and scoped mention labels.","Hidden and flagged messages require an explicit admin moderation read contract."]},{"id":"community.messages.post","toolName":"fanful_listener_community_message_post","status":"ready","enabled":false,"summary":"Post a visible message, or a lightweight reply, as the authenticated listener.","endpoint":{"href":"https://fanful.net/api/agent/community-chat","method":"POST","auth":"signed-in-listener","description":"Post a visible community message as the signed-in/scoped listener with explicit agent confirmation."},"confirmation":{"required":true,"kind":"public-message","reason":"Community messages are externally visible to listeners."},"audit":{"required":true,"currentRecord":"community_chat_messages","attribution":"Stored with the signed-in user's id and display name."},"input":{"channelId":{"type":"string","required":true,"description":"Active community channel id."},"body":{"type":"string","required":true,"minLength":1,"maxLength":600},"replyToMessageId":{"type":"string","required":false,"description":"Visible parent message id in the same Laurel community channel."},"targetSummary":{"type":"string","required":true,"description":"Human-readable posting summary used in the exact confirmation text."},"confirmationText":{"type":"string","required":true,"description":"Must exactly match: I confirm listener.community-message-post for <targetSummary>."},"acknowledgedRisk":{"type":"boolean","required":true,"description":"Must be true because this creates listener-visible speech."},"idempotencyKey":{"type":"string","required":true,"description":"Fresh key for duplicate-safe retries."},"auditCorrelationId":{"type":"string","required":true,"description":"Caller-visible audit correlation id."},"reason":{"type":"string","required":true,"description":"Reason stored with the agent response audit summary."}},"boundaries":["Uses the same signed-in session and body validation as web/native community chat.","Replies can target visible messages only; hidden, flagged, cross-channel, and cross-artist targets are rejected.","Mention labels are parsed from the submitted body and notification routing is delegated to community-chat-notifications.v1.","Automation tokens cannot post as a listener."]},{"id":"community.messages.react","toolName":"community_messages_react","status":"ready","enabled":false,"summary":"Apply or remove the authenticated listener's reaction on one visible community message.","endpoint":{"href":"https://fanful.net/api/community/messages/reactions","method":"POST","auth":"signed-in-listener","description":"Toggle the signed-in listener's reaction on a visible Laurel community message."},"confirmation":{"required":true,"kind":"public-reaction","reason":"Reactions change public aggregate counts and the caller's visible reaction state."},"audit":{"required":true,"currentRecord":"community_chat_message_reactions","attribution":"Stored with the signed-in user's id; each user can toggle only their own reaction."},"input":{"messageId":{"type":"string","required":true,"description":"Visible community message id."},"reaction":{"type":"enum","required":true,"values":["heart","👍","👏","🔥","😂","✨"]}},"boundaries":["Uses the same signed-in session as web/native reaction toggles.","Only visible Laurel community messages can be reacted to.","Hidden or flagged messages return unavailable rather than exposing moderation context.","Automation tokens cannot react as listeners."]},{"id":"community.messages.react-custom-emoji","toolName":"community_messages_react_custom_emoji","status":"ready","enabled":false,"summary":"Apply or remove the authenticated listener's community-scoped custom emoji reaction.","endpoint":{"href":"https://fanful.net/api/community/messages/reactions","method":"POST","auth":"signed-in-listener","description":"Toggle a visible custom emoji by customEmojiId on a visible community message."},"confirmation":{"required":true,"kind":"public-reaction","reason":"Custom emoji reactions change public aggregate counts and usage analytics."},"audit":{"required":true,"currentRecord":"community_chat_message_reactions.custom_emoji_id","attribution":"Stored with the signed-in user and active community custom emoji id."},"input":{"messageId":{"type":"string","required":true,"description":"Visible community message id."},"customEmojiId":{"type":"string","required":true,"description":"Active custom emoji id in the same creator community."}},"boundaries":["Custom emoji reactions require an active community emoji scoped to the message's artist/community.","The reaction row stores custom_emoji_id so My emojis can compute usage counts and top users without parsing display text.","Hidden, removed, or cross-community custom emojis are unavailable to ordinary callers."]},{"id":"community.socket.connect","toolName":"community_socket_connect","status":"ready","enabled":false,"summary":"Connect to realtime community channel presence and message events.","endpoint":{"href":"wss://fanful.net/api/community/socket","method":"WS","auth":"optional-session","description":"Connect with a short-lived token minted by /api/community/socket-token.","query":["token"]},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":false,"currentRecord":null,"attribution":"Presence display name comes from the socket token request."},"input":{"token":{"type":"string","required":true,"description":"Short-lived socket token."}},"boundaries":["Call /api/community/socket-token first; the manifest never includes raw tokens.","Posting through the socket still requires a signed-in listener session."]},{"id":"community.socket.typing","toolName":"community_socket_typing","status":"ready","enabled":false,"summary":"Send typing-start or typing-stop presence over the realtime community socket.","endpoint":{"href":"wss://fanful.net/api/community/socket","method":"WS","auth":"signed-in-listener","description":"Send { type: 'typing', typing: boolean } after connecting with a short-lived community socket token.","query":["token"]},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":false,"currentRecord":null,"attribution":"Typing presence is ephemeral, channel-scoped, and expires without a durable D1 record."},"input":{"type":{"type":"enum","required":true,"values":["typing"]},"typing":{"type":"boolean","required":true,"description":"true while composing; false when stopped or sent."}},"boundaries":["Typing presence requires a signed-in listener who can post to the selected channel.","The Durable Object expires typing participants after roughly six seconds and broadcasts chat-typing payloads scoped to the channel.","Clients must suppress the current viewer when rendering names such as 'Rob is typing...'."]},{"id":"community.socket-token.create","toolName":"community_socket_token_create","status":"ready","enabled":false,"summary":"Mint a short-lived community-channel socket token.","endpoint":{"href":"https://fanful.net/api/community/socket-token","method":"POST","auth":"optional-session","description":"Issue a short-lived community chat WebSocket token."},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":false,"currentRecord":null,"attribution":"Token claims include the submitted viewer display name and signed-in user when present."},"input":{"channelId":{"type":"string","required":true,"description":"Active community channel id."},"viewerId":{"type":"string","required":true,"description":"Client-scoped viewer id for presence."},"displayName":{"type":"string","required":false}},"boundaries":["Tokens are short-lived and should not be logged into model-visible transcripts."]},{"id":"community.custom-emojis.list","toolName":"community_custom_emojis_list","status":"ready","enabled":false,"summary":"List active community custom emoji plus the caller's My emojis usage stats.","endpoint":{"href":"https://fanful.net/api/community/custom-emojis","method":"GET","auth":"signed-in-listener","description":"List active community emoji and caller-owned My emojis analytics."},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":false,"currentRecord":"community_chat_custom_emojis + community_chat_message_reactions","attribution":"Read-only usage stats are derived from custom emoji ownership and reaction rows."},"input":{"artistId":{"type":"string","required":false,"description":"Future multi-community selector; Laurel is implicit in V1."},"mine":{"type":"boolean","required":false,"description":"When true, return emojis uploaded by the signed-in user."}},"boundaries":["Only active emojis are shown in the community picker.","My emojis includes hidden/removed ownership state only for the uploader and authorized admins.","Usage totals and top users are scoped to one creator community, not global across Fanful."]},{"id":"community.custom-emojis.upload","toolName":"community_custom_emojis_upload","status":"ready","enabled":false,"summary":"Upload a community-scoped custom emoji for all members to use.","endpoint":{"href":"https://fanful.net/api/community/custom-emojis","method":"POST","auth":"signed-in-listener","description":"Upload a validated custom emoji image for the Laurel community."},"confirmation":{"required":true,"kind":"public-message","reason":"Uploaded custom emoji become visible and reusable by the creator community."},"audit":{"required":true,"currentRecord":"community_chat_custom_emojis","attribution":"Stored with uploader_user_id and artist/community scope."},"input":{"name":{"type":"string","required":true,"pattern":"^[a-z0-9][a-z0-9_-]{1,31}$","description":"Slack-style shortcode without surrounding colons, unique within the community."},"file":{"type":"file","required":true,"maxBytes":262144,"contentTypes":["image/png","image/jpeg","image/gif","image/webp"]}},"boundaries":["Binary image bytes stay out of model-visible agent inputs and logs.","Uploads are stored in the MEDIA R2 binding under a community emoji key and referenced by D1 metadata.","Moderation can hide/remove an emoji without deleting historical reaction records."]},{"id":"community.custom-emojis.remove","toolName":"community_custom_emojis_remove","status":"ready","enabled":false,"summary":"Remove one caller-owned custom emoji from the community picker.","endpoint":{"href":"https://fanful.net/api/community/custom-emojis","method":"PATCH","auth":"signed-in-listener","description":"Mark one caller-owned custom emoji as removed."},"confirmation":{"required":true,"kind":"public-message","reason":"Removing a custom emoji changes what the community can reuse while preserving historical reactions."},"audit":{"required":true,"currentRecord":"community_chat_custom_emojis.status/hidden_at/hidden_by","attribution":"Only the uploader can remove their own custom emoji through this listener route."},"input":{"emojiId":{"type":"string","required":true,"description":"Caller-owned custom emoji id."},"status":{"type":"enum","required":true,"values":["removed"]}},"boundaries":["Removal hides the emoji from future picker results but does not delete prior message reaction records.","Cross-community or non-owned emoji ids return unavailable without exposing private ownership details."]},{"id":"community.notifications.update","toolName":"community_notifications_update","status":"ready","enabled":false,"summary":"Read or update the authenticated listener's community chat mention and digest preferences.","endpoint":{"href":"https://fanful.net/api/account/notification-preferences","method":"PATCH","auth":"signed-in-listener","description":"Update first-party notification preferences, including community chat channel membership, mutes, mentions, and digests."},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":false,"currentRecord":"community_chat_memberships","attribution":"Preference writes are scoped to the signed-in listener account."},"input":{"communityChat":{"type":"object","required":true,"description":"Patch shape: { channels: { [channelId]: { joined?, muted?, mentions?: { email?, push? }, digest?: { email?, push?, cadence?: 'off' | 'daily' | 'weekly' } } } }."}},"boundaries":["Uses the Comments notification category as the global gate.","Never routes mentions or digests to listeners who have not joined the channel or have muted it."]},{"id":"community.notifications.read","toolName":"community_notifications_read","status":"ready","enabled":false,"summary":"Read the signed-in listener's in-app notification events for reactions, replies, shows, and donation thanks.","endpoint":{"href":"https://fanful.net/api/account/notifications","method":"GET","auth":"signed-in-listener","description":"Read the signed-in listener's in-app notification-center events."},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":false,"currentRecord":"community_notification_events","attribution":"Events are private to the signed-in listener recipient."},"input":{"status":{"type":"enum","required":false,"values":["unread","read","archived"]},"limit":{"type":"integer","required":false,"min":1,"max":100}},"boundaries":["Listener events must never expose another listener's private notifications.","Events include enough related ids and actionHref metadata for clients to deep-link to the message, live event, donation, or account item."]},{"id":"community.notifications.mark","toolName":"community_notifications_mark","status":"ready","enabled":false,"summary":"Mark signed-in listener notification-center events read or archived.","endpoint":{"href":"https://fanful.net/api/account/notifications","method":"PATCH","auth":"signed-in-listener","description":"Mark the signed-in listener's own notification events read or archived."},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":true,"currentRecord":"community_notification_events.status/read_at/archived_at","attribution":"Only rows for the signed-in listener recipient can be updated."},"input":{"ids":{"type":"array","required":true,"items":"string","maxItems":100},"action":{"type":"enum","required":true,"values":["read","archive"]}},"boundaries":["Listener mark actions are recipient-scoped and ignore another listener's event ids.","Archiving also records read_at so unread counts cannot stay inflated after an archive."]},{"id":"community.publisher-notifications.read","toolName":"community_publisher_notifications_read","status":"ready","enabled":false,"summary":"Read creator/admin notification events that need publisher action.","endpoint":{"href":"https://fanful.net/api/admin/notifications","method":"GET","auth":"artist-admin","description":"Read creator/admin notification-center events that need publisher action."},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":false,"currentRecord":"community_notification_events","attribution":"Publisher notification reads require existing Laurel admin authorization."},"input":{"status":{"type":"enum","required":false,"values":["unread","read","archived"]},"limit":{"type":"integer","required":false,"min":1,"max":100}},"boundaries":["Publisher events are artist/admin scoped and should not appear in listener notification centers.","Moderation review and creator-action events should deep-link to the relevant admin surface, not a public listener page."]},{"id":"community.publisher-notifications.mark","toolName":"community_publisher_notifications_mark","status":"ready","enabled":false,"summary":"Mark creator/admin notification-center events read or archived.","endpoint":{"href":"https://fanful.net/api/admin/notifications","method":"PATCH","auth":"artist-admin","description":"Mark creator/admin notification-center events read or archived."},"confirmation":{"required":false,"kind":"none","reason":null},"audit":{"required":true,"currentRecord":"community_notification_events.status/read_at/archived_at","attribution":"Publisher notification updates require Laurel admin authorization."},"input":{"ids":{"type":"array","required":true,"items":"string","maxItems":100},"action":{"type":"enum","required":true,"values":["read","archive"]}},"boundaries":["Publisher notification updates stay scoped to Laurel admin/creator-action events.","Listener notification-center events are not updated through the publisher route."]},{"id":"community.messages.moderate","toolName":"community_messages_moderate","status":"ready","enabled":false,"summary":"Hide, flag, or restore a Laurel community message as an authorized admin or automation.","endpoint":{"href":"https://fanful.net/api/admin/community-chat/messages","method":"PATCH","auth":"artist-admin-or-approved-automation","description":"Moderate one community message."},"confirmation":{"required":true,"kind":"moderation","reason":"Moderation changes alter what listeners can see."},"audit":{"required":true,"currentRecord":"community_chat_messages.hidden_by/hidden_at plus realtime moderation event","attribution":"Stored with the admin email/name or Automation token identity."},"input":{"id":{"type":"string","required":true,"description":"Community message id."},"status":{"type":"enum","required":true,"values":["visible","hidden","flagged"]}},"boundaries":["Requires Laurel artist-admin or owner access, or the approved automation token.","Agents should confirm before hiding/restoring listener-visible messages."]},{"id":"community.channels.manage","toolName":"fanful_creator_community_channel_create","status":"ready","enabled":false,"summary":"Create an additional Laurel community channel, including optional membership-tier access metadata.","endpoint":{"href":"https://fanful.net/api/admin/community-chat","method":"POST","auth":"artist-admin-or-approved-automation","description":"Create an unlocked active community channel through the confirmed agent wrapper."},"confirmation":{"required":true,"kind":"moderation","reason":"Channel creation changes the public listener surface and access rules."},"audit":{"required":true,"currentRecord":"community_chat_channels/community_chat_admin_actions/agent_write_idempotency_keys","attribution":"Stored with the admin email/name or Automation token identity plus agent envelope metadata."},"input":{"name":{"type":"string","required":true,"maxLength":80},"description":{"type":"string","required":false,"maxLength":240},"accessMode":{"type":"enum","required":false,"values":["all-logged-in-listeners","membership-tiers"]},"tierIds":{"type":"string[]","required":false,"description":"Active membership tier ids when accessMode is membership-tiers; empty means any active membership."},"observedChannelIds":{"type":"string[]","required":true,"description":"All channel ids from the latest community-chat/admin read."},"targetSummary":{"type":"string","required":true},"confirmationText":{"type":"string","required":true,"description":"Must exactly match: I confirm creator.community.channel-create for <targetSummary>."},"acknowledgedRisk":{"type":"boolean","required":true},"idempotencyKey":{"type":"string","required":true},"auditCorrelationId":{"type":"string","required":true},"reason":{"type":"string","required":true}},"boundaries":["Honors the same unlock count and 4-channel cap, including #general, as the web admin surface.","Membership-tier access can reference active tiers only; this tool never creates tiers, changes prices, grants memberships, or sends notifications.","Agents should prefer proposing channel changes when the user's intent is ambiguous."]},{"id":"community.channels.update","toolName":"fanful_creator_community_channel_update","status":"ready","enabled":false,"summary":"Rename, describe, archive, or update tier access for a non-default Laurel community channel.","endpoint":{"href":"https://fanful.net/api/admin/community-chat","method":"PATCH","auth":"artist-admin-or-approved-automation","description":"Update a channel name/description/status/access through the confirmed agent wrapper."},"confirmation":{"required":true,"kind":"moderation","reason":"Channel settings and permission changes alter the listener community surface."},"audit":{"required":true,"currentRecord":"community_chat_channels/community_chat_admin_actions/agent_write_idempotency_keys","attribution":"Stored with the admin email/name or Automation token identity plus agent envelope metadata."},"input":{"channelId":{"type":"string","required":true,"description":"Community channel id."},"name":{"type":"string","required":false,"maxLength":80},"description":{"type":"string","required":false,"maxLength":240},"status":{"type":"enum","required":false,"values":["archived"]},"accessMode":{"type":"enum","required":false,"values":["all-logged-in-listeners","membership-tiers"]},"tierIds":{"type":"string[]","required":false,"description":"Active membership tier ids when accessMode is membership-tiers; empty means any active membership."},"observedUpdatedAt":{"type":"string","required":true,"description":"The community_chat_channels.updated_at value from the latest read."},"targetSummary":{"type":"string","required":true},"confirmationText":{"type":"string","required":true,"description":"Must exactly match: I confirm creator.community.channel-update for <targetSummary>."},"acknowledgedRisk":{"type":"boolean","required":true},"idempotencyKey":{"type":"string","required":true},"auditCorrelationId":{"type":"string","required":true},"reason":{"type":"string","required":true}},"boundaries":["The required #general channel cannot be renamed, archived, or membership-gated.","Membership-tier access can reference active tiers only; this tool never creates tiers, changes prices, grants memberships, or sends notifications.","Agents should confirm before renaming or archiving listener-visible channels."]},{"id":"community.creator-automation.propose","toolName":"community_creator_automation_propose","status":"ready","enabled":false,"summary":"Create a creator-approved community automation rule with confirmation, attribution, audit, and revocation.","endpoint":{"href":"https://fanful.net/api/admin/community-chat/automation-rules","method":"POST","auth":"artist-admin-or-approved-automation","description":"Create a confirmed community automation rule; PATCH the same endpoint to pause or archive it."},"confirmation":{"required":true,"kind":"creator-speech","reason":"Rules that speak for Laurel need explicit creator approval, visibility, and revocation."},"audit":{"required":true,"currentRecord":"community_chat_automation_rules plus community_chat_admin_actions","attribution":"Emitted replies are stored as automation messages with creator-approved attribution, rule id, trigger id, and audit rows."},"input":{"channelId":{"type":"string","required":true,"description":"Active community channel id."},"triggerPhrase":{"type":"string","required":true,"minLength":2,"maxLength":120},"responseBody":{"type":"string","required":true,"minLength":1,"maxLength":600},"attributionLabel":{"type":"string","required":true,"maxLength":120},"status":{"type":"enum","required":true,"values":["paused","active","archived"]},"confirmed":{"type":"boolean","required":true,"description":"Required before an active rule can emit replies."}},"boundaries":["Active rules emit public replies through community-chat-automation-replies.v1 only after confirmation.","Archived rules stop future replies; emitted replies keep rule id, trigger id, attribution, and audit linkage.","The processor is Laurel-scoped until multi-community membership boundaries exist."]}],"notificationRouting":{"version":"community-chat-notifications.v1","categoryGate":"comments","membershipTable":"community_chat_memberships","membershipRequired":true,"muteStopsDelivery":true,"preferenceEndpoint":{"href":"https://fanful.net/api/account/notification-preferences","method":"GET","auth":"signed-in-listener","description":"Read notificationPreferences and communityChat channel notification settings; PATCH the same endpoint to update them."},"routeResolvers":{"mentions":"resolveCommunityChatMentionNotificationRoutes matches @mentions to joined, unmuted channel members and gates email/push through Comments preferences and device/email readiness.","digests":"resolveCommunityChatDigestNotificationRoutes returns joined, unmuted digest audiences for daily or weekly cadence without notifying non-members."},"deliveryStatus":"Routing contracts are implemented; actual outbound email/push workers must consume these routes instead of sending directly from chat write paths."},"policy":{"writeConfirmation":"Require explicit user confirmation for public posts, moderation changes, channel management, and creator-speech automation rules.","attribution":"Listener posts use the signed-in listener display name. Admin moderation uses admin identity. Creator automations are visibly attributed, linked to their rule and trigger, audited, and revocable.","secretHandling":"Do not expose raw socket tokens, admin tokens, payment identifiers, private invite links, or hidden moderation history in model-visible logs."},"serverGaps":[{"id":"community.notification-delivery-worker","relatedIssue":640,"status":"partial","note":"Mention and digest recipient routing now exists behind listener preferences and membership mutes; a later worker should perform outbound email/push delivery and digest scheduling from those routes."},{"id":"community.admin-audit-log","relatedIssue":616,"status":"partial","note":"Moderation records currently store hidden_by/hidden_at and broadcast realtime changes; broader admin audit events should be added before exposing high-volume autonomous moderation."}]}