morphit/docs/adr/0012-cross-post-flow.md

232 lines
10 KiB
Markdown

# ADR-0012 — Post-first-trade cross-post flow to Blurt
**Status:** Accepted (implemented in `apps/web/src/lib/syndication/publish.ts`)
**Date:** 2026-04-19 (proposed); 2026-05-06 (status updated)
**Deciders:** project maintainer
**Related:** ADR-0010 (key custody), ADR-0011 (dynamic fee model)
> Originally a stub with three UNDECIDED design questions. All
> three resolved during Phase 5 implementation; the resolutions
> are recorded in the **Design questions resolved** section near
> the bottom of this document. The Decision section below is
> the resolved shape now in code.
## Context
Phase 4c delivered the delayed welcome bonus: when a Morphit
user completes their first trade (evidenced by counterparty
`morphit_feedback_v1` submission), the relay sends them 10 BLURT
liquid + 10 BP as a welcome gift. The bonus is a concrete moment
of delight for the user.
Phase 5 wants to convert that moment into ecosystem reach. The
thesis: if Morphit's users post about their first completed trade
on Blurt, those posts become organic discovery vectors for new
users browsing blurt.blog, blurt.media, and the wider Blurt
social layer. This is content marketing via user-generated
authenticity rather than paid promotion.
The mechanism: right after the welcome bonus arrives, offer the
user a pre-filled "I traded on Morphit" blog post they can
review, edit, and publish to their own Blurt account. The user
owns the posting key that would sign the post; Morphit never
gets custody of content they didn't author.
### Constraints from Morphit principles
- **Consent-first.** Users must opt in per-post, explicitly. No
auto-posting. No "quiet default to on."
- **Counterparty privacy.** The post must not reveal the
counterparty's account name, the trade amount, the asset
traded, or timing precise enough to identify the trade from
on-chain data. A trade-completion post that says "I just
bought $500 of BTC from @alice using Zelle" is unacceptable
— that's doxxing the counterparty.
- **Non-custody stays non-custody.** The post is signed by the
user's posting key (JIT-decrypted). Morphit never sees or
persists the post content server-side.
- **Reversibility.** If a user regrets the post, they can
delete it via any Blurt frontend. No Morphit-side lock-in.
### Technical state of play
- A feedback-submission UI does NOT exist in the Phase 4 frontend.
The welcome bonus fires when the counterparty's feedback is
indexed, but there's no UI for the original poster to submit
THEIR feedback yet. Cross-post goes on the feedback-submit
screen (the logical "trade complete" moment), so that UI is a
hard prerequisite.
- The user's posting key is JIT-decrypted whenever they're
composing an order or cancelling one. For cross-posting, we
need a `custom_json`-like op (actually `comment` — the Blurt
blog post op).
- The 10-locale i18n system (541 strings) needs ~5 new strings
for the cross-post UI, plus the post-template body and title
in each locale.
## Decision
**When:** On the feedback-submit confirmation screen, offer
(opt-in) to cross-post to Blurt. The offer shows once — if
declined, don't re-prompt.
**What:** A short Blurt comment op (i.e., a blog post) with:
- A title in the user's locale (10 localized versions; UNDECIDED
whether fully fixed or fill-in-blank — see Open Questions).
- A body crediting Morphit, mentioning the platform link, and
tagged `#morphit #p2p #privacy`. No counterparty info. No
concrete amounts. No asset specificity unless the user
explicitly toggles "include asset" (default off).
- Posted into `@{user}/morphit-first-trade-{random}` — the
user's own account namespace.
**Key handling:** Require an explicit password re-entry before
signing the `comment` op, even if the posting key is already
unlocked in the session. This makes the permanence of the post
visible and deliberate. Rationale: unlike short-lived Morphit
ops (orders, cancellations), a blog post is public and
permanent — users should feel the weight of that choice.
**Resolved 1 — Post template:** Fixed template per locale. No
in-line user editing; the post is fired in the same client turn
as the feedback broadcast (Post A) or order broadcast (Post B).
See "Design questions resolved" below.
**Resolved 2 — Community posting:** Both, by post type. Post A
goes to the @morphit Blurt community (`parent_permlink =
"blurt-176570"`). Post B goes to the user's personal feed with
parent_permlink "morphit" (the project tag). See "Design
questions resolved" below.
**Resolved 3 — Schedule:** Immediate. Fired in the same client
turn as the triggering action. Replaced the originally-considered
deferred-syndication-queue model that was scaffolded but never
shipped. See "Design questions resolved" below.
## Alternatives considered
- **Auto-post (opt-out).** Rejected outright on the consent-
first principle. Any auto-posting under user accounts — even
"silent" metadata ops — erodes the trust that Morphit never
acts as the user.
- **Post from @morphit-relay on the user's behalf.** Rejected
because it would require custodial authority over content
posted under the user's name (even if semantically attributed
to Morphit, the chain record would show @morphit-relay posting
about users' trades, which is creepy and off-brand for a
non-custodial protocol).
- **Off-chain announcement via Morphit-side database.** We
could maintain a "recent trades" page on the Morphit website
that's populated server-side based on feedback events.
Rejected because it doesn't use Blurt's network effect — the
whole point is to push content INTO the Blurt social layer,
not to host it ourselves.
- **Share button that opens a pre-filled compose window on
blurt.blog.** A lower-effort version: instead of signing
directly in Morphit, we redirect the user to a blurt.blog
compose URL with the title/body pre-filled. Rejected because:
(a) it breaks for users without a blurt.blog session
(they'd have to log in again), and (b) it creates a UX seam
right at the emotional-high moment after a successful trade.
## Consequences
### Positive
- Organic discovery: every cross-posted first trade is a Morphit
mention on the Blurt social layer. If 30% of first-time users
opt in, and Morphit gains 1000 first-time users in a year,
that's 300 organic posts.
- Reinforces the non-custodial narrative: Morphit gives you
something to share about your experience, signed under your
own key, that you fully own.
- Closes the "what happened after the trade" loop: users
currently complete their first trade and then don't interact
with the site again until their next trade. The cross-post
gives them a closing action that feels rewarding.
### Negative
- Exposes a new vector for misinformation: if Morphit's
template copy overclaims (e.g., "I made $1000 trading
crypto!"), we're embedding it in user voices. Copy needs
to be carefully neutral.
- 10-locale translation work for the template — adds to
i18n maintenance burden.
- Prerequisite feedback-submit UI work (medium-sized frontend
feature) must ship before this can land.
- Cross-posts from newly-minted accounts with low BP might get
filtered as spam on Blurt frontends. We can't control Blurt's
spam filters; some percentage of posts won't land.
### Follow-up work
- Feedback-submit UI in the frontend (prerequisite).
- ADR-0013 must resolve whether a dedicated Morphit community
exists before we can target posts there.
- 10-locale translation pass for the template.
- Analytics: track opt-in rate. If under 10%, the feature
isn't earning its implementation cost.
## Design questions resolved (2026-05-06)
### Q1 — Post template: fully fixed
**Decision:** Fixed template per locale, generated programmatically
from `apps/web/src/lib/syndication/publish.ts`. Hardcoded image
URLs (`IMAGE_FIRST_TRADE`, `IMAGE_ORDER_POST`) point at images
pre-uploaded to Blurt's image host. No in-line user editing; the
post is fired in the same client turn as the triggering action.
**Why:** Removes a UX friction point (no "review before post"
modal that users habitually dismiss without reading), removes the
attack surface of user-controlled HTML/markdown landing under
the @morphit-community feed, and keeps the "post Morphit talks
about itself in a uniform way" branding consistent across operators.
**User control:** The opt-out is per-feature (Settings → "Auto-
announce my first trade") rather than per-post. Per-order
syndication (Post B) is already opt-in via a checkbox on `/post`.
### Q2 — Community vs personal: both, by post type
**Decision:**
- **Post A (first-trade announcement)** → @morphit Blurt community
(`parent_permlink = "blurt-176570"`). This is the discovery
channel where Blurt users browsing the @morphit feed see new
Morphit users' first-trade celebrations.
- **Post B (per-order syndication)** → user's personal blog with
parent_permlink `"morphit"` (the project tag). This is the
user's own marketing of their open trade, posted to their own
followers, discoverable by anyone browsing the `morphit` tag.
**Why:** Post A is project-branded promotion; the community feed
makes that explicit and gives the @morphit community account a
reason to exist as a content hub. Post B is user-driven; their
personal blog is the right venue for their own order.
### Q3 — Schedule: immediate
**Decision:** Both posts fire in the same client turn as the
triggering action (feedback broadcast for Post A, order broadcast
for Post B). No deferred-syndication-queue, no indexer state, no
"pending" banner.
**Why:** The originally-proposed 30-60s scheduled delay was
scaffolded as a deferred-queue with cancel UI, but in practice
nobody used the cancel and the indexer-state added attack surface
for syndication-queue tampering. Replacing it with immediate
firing eliminated ~200 lines of scaffolding code (publish.ts §
"Replaces the old deferred-syndication-queue model"). Idempotency
via deterministic permlink — a retry of the same trigger lands
on the same Blurt post (edit, not duplicate).
## References
- `apps/web/src/lib/syndication/publish.ts` — implementation
- `apps/web/src/lib/utils/syndicationPrefs.ts` — opt-out store
- PHASE-5-BACKLOG.md item 1
- ADR-0010 — key custody (constrains posting-key handling)
- ADR-0011 — dynamic fee model (defines the welcome bonus
that this feature emotionally follows from)