Mastodon platform integration - design
Status: approved 2026-07-17. Tracks GitHub issue #128 (third outreach platform, chosen after Reddit + Hacker News).
Summary
Add Mastodon as the third outreach platform. Unlike Reddit (Playwright-scraped, anonymous, extension-dependent for replies), Mastodon exposes an open REST API, so the adapter is a thin authenticated HTTP client: no Playwright, no stealth stack, no Chrome extension. Reply detection runs fully server-side in the daemon.
Pitchbox stays human-in-the-loop: the agent researches and drafts, the human approves. Sending is manual by default (like Reddit/HN); a campaign can opt in to auto-posting via the API on approval.
Decisions (settled 2026-07-17)
- Send model: manual by default; a per-campaign
autoPostflag lets Pitchbox post via the API on approval. Both paths run throughevaluateDraftSend(blocklist + quota) first. - Auth: per-account, paste an access token. An account carries its own
instanceUrl+ an access token created in that instance's developer settings, stored encrypted withENCRYPTION_KEY. No OAuth callback infrastructure (YAGNI). - Scenarios (v1): all three, at parity with Reddit -
scout->DM,commenter,poster. - Reply detection: a real server-side
MastodonReplyReader(polls/api/v1/notificationsfor mentions). The daemon reply-poller, inert for Reddit, is real for Mastodon. - Guardrails / tone: conservative. The fediverse is hostile to cold marketing/DMs; playbooks prioritize genuine, contextual replies, discourage cold DMs, and respect
#nobotand opt-outs.#nobotis a hard, non-configurable skip in the scout.
Scenario mapping (draft.kind)
dm-> adirect-visibility status mentioning the target (Mastodon "DM"; note it is not private/E2E).comment-> a reply status (in_reply_to_id= the target status), visibility public or unlisted.post-> a top-level public status (toot) with hashtags.
Architecture
Platform + accounts
- Seed a
mastodonrow inplatforms(seed:core). - Reuse the
accountstable. Add aninstance_urlcolumn; store the access token encrypted (reuse the existing encrypted-credential field or add one).handleis the fully qualified@user@instance. On connect, validate withGET /api/v1/accounts/verify_credentials. - Connection UI: a Settings/Accounts form to paste instance URL + token, with instructions to create an app in the instance's dev settings.
Adapter (shared/src/platforms/mastodon/)
types.ts: Mastodon API shapes (Status, Account, Notification, Context).client.ts: fetch-based REST client, bearer token, honorsX-RateLimit-*headers with backoff. Methods:verifyCredentials,hashtagTimeline(tag, sinceId),getStatus(id),postStatus({status, inReplyToId?, visibility}),notifications({sinceId, types:['mention']}).scout.ts: target discovery via hashtag timelines (Mastodon lacks reliable full-text search) plus keyword filtering, honoring the#nobothard-rule (skip authors whose bio/fields contain#nobot/nobot), recency, and the blocklist.reply-reader.ts:MastodonReplyReader implements ReplyReader- reads mentions from/api/v1/notificationssince the cursor, maps toReply[](targetUser,at,preview). Registered in the daemon's reply-reader registry formastodon(real, not Null).index.ts: exports + registration.
MCP tool surface
Mirror the Reddit/HN pattern: expose Mastodon actions as mcp__pitchbox__mastodon_* tools in cli/src/mcp/server.ts (backed by the adapter), so playbooks drive them. At minimum a scout/search tool and a post/reply tool; ids stay session-bound (never chosen by the agent), consistent with the existing surface.
Send path (human-in-the-loop, two modes)
- Manual (default): draft -> approve -> a "Open on Mastodon" action (copies the text and opens the account's instance) -> "Mark as sent". No extension needed.
- Auto-post (
autoPostcampaign flag): on approve, Pitchbox callspostStatus(mapped visibility +inReplyToIdfor comments), stores the returned status id/URL, auto-marks the draftsent, and logscontact_history. - Both run through
evaluateDraftSend(blocklist + quota) before anything is sent.
Reply matching
- On send, store the posted Mastodon status id on the draft (a new
platform_post_idcolumn, or reuseplatform_comment_id). The reply-reader matches a mention whosein_reply_to_idequals our status id (or from the target user) -> draftreplied+ message recorded. Fully server-side in the daemon reply-poller (which becomes real for Mastodon).
Quota + blocklist
- Quota: add
mastodontoquota_defaultswith conservative per-day/per-week limits for dm/comment/post. Per-account limits already supported. - Blocklist: reuse kinds
user(a@handle) andkeyword(skip statuses containing it).#nobotis an implicit hard-rule in the scout, not a blocklist entry.
Playbooks
Three conservative markdown playbooks driving the MCP tools: mastodon-scout, mastodon-commenter, mastodon-poster. Tone baked in: genuine/contextual replies, cold DMs discouraged, honor #nobot and opt-outs, soft rates.
What is NOT needed (vs Reddit)
No Playwright/stealth, no anonymous-scrape env, no Chrome extension for Mastodon. Only authenticated REST.
Testing
Unit tests with mocked fetch (no live API calls): the client (incl. rate-limit backoff), scout filtering (#nobot, blocklist, recency, keyword), reply-reader mapping, scenario->API mapping, the auto-post path, and quota/blocklist gating on the send path.
Implementation breakdown (issues)
Sequenced with dependencies; most are file-disjoint for parallel execution once the foundation lands.
- MAS-1 (foundation):
mastodonplatform seed; account model (instance_url+ encrypted token, migration); connect UI +verify_credentials. - MAS-2 (foundation): Mastodon REST client + types (mock-fetch tested). Parallel with MAS-1.
- MAS-3: scout (hashtag-timeline discovery +
#nobot/keyword/blocklist filtering) +mastodon_scoutMCP tool. Depends on MAS-2. - MAS-4:
MastodonReplyReader+ daemon registration formastodon. Depends on MAS-2. - MAS-5: send path - scenario->API mapping, manual + auto-post (per-campaign flag),
platform_post_idmigration + reply matching,evaluateDraftSendintegration, post/reply MCP tool. Depends on MAS-1 + MAS-2. - MAS-6: the three conservative playbooks. Mostly independent (drafts against the MCP tool names).
- MAS-7: quota defaults + blocklist handling for
mastodon. Small, mostly independent.
Wave 1: MAS-1, MAS-2. Wave 2: MAS-3, MAS-4, MAS-5. Wave 3: MAS-6, MAS-7.
Out of scope (v1)
OAuth flow (paste-token only), full-text search beyond hashtags, Mastodon-native polls/media, cross-instance account discovery beyond federation-visible timelines.