morphit/docs/SYNDICATION-CHECKPOINT.md

223 lines
11 KiB
Markdown

# Syndication (ADR Q3) — POST-PIVOT CHECKPOINT
**Status:** Phase 1 + Phase 3 complete as of 2026-04-22 resume session.
Phase 2 pending the feedback-broadcast UI.
## The model (authoritative, implemented)
### Post A — "I joined Morphit" (automatic, once per account, community)
- **When**: client-side, after the user broadcasts feedback on their
first trade (which is the forced BLURT BUY per Phase 3).
- **Where**: @morphit community (account: `blurt-176570`). Op's
`parent_permlink` = `"blurt-176570"`.
- **No opt-in.** Automatic onboarding step.
- **Signed by**: user's own posting key.
- **Permlink**: `morphit-first-trade-<feedback-trx-id-truncated>`,
deterministic so retries are idempotent.
- **Title**: `I just completed my first trade with @{seller} on Morphit!`
- **Body**: includes `IMAGE_FIRST_TRADE` markdown image at top,
Morphit tagline, link to user's profile at `https://morphit.io/@{username}`.
- **json_metadata**: `app: "morphit/0.1.0"`, `format: "markdown"`,
`tags: ["morphit", "first-trade", "p2p-trading"]`,
`image: ["<IMAGE_FIRST_TRADE>"]`.
- **Helper**: `publishFirstTradePost(live, {seller, feedbackTrxId})`
in `apps/web/src/lib/syndication/publish.ts`. Returns
`PublishResult` (ok/error). Typically fire-and-forget.
### Post B — per-order announcement (opt-in, personal blog)
- **When**: IMMEDIATELY on order submit, iff the user ticked the
checkbox on /post. Fires in the same client turn as the order
broadcast, not on fill.
- **Where**: user's own blog. Op's `parent_permlink` = `"morphit"`
(Blurt convention: first tag = category). Shows under the
`#morphit` discovery feed AND on the user's own profile.
- **Opt-in**: checkbox on /post labeled "Syndicate this order to my
Blog too (Free)".
- **Signed by**: user's own posting key.
- **Permlink**: `morphit-announce-<order_permlink>`, retry-safe.
- **Title**: `I'm {buying|selling} {ASSET1} with {ASSET2}. Want to trade?`
- **Body**: includes `IMAGE_ORDER_POST` markdown image at top,
repeat of title as opening line, Morphit tagline, link to
`https://morphit.io/@{username}/{permlink}`.
- **json_metadata**: `app: "morphit/0.1.0"`, `format: "markdown"`,
`tags: ["morphit", "p2p-trading", "<asset_lowercase>"]`,
`image: ["<IMAGE_ORDER_POST>"]`.
- **Helper**: `publishOrderPost(live, {orderPermlink, side, asset, counterAsset})`
in `apps/web/src/lib/syndication/publish.ts`. Returns explicit
`PublishResult` so the /post success card can show status.
## Completed in Phase 1
- ✅ Old deferred-syndication machinery deleted:
- `apps/web/src/lib/syndication/queue.ts`
- `apps/web/src/lib/syndication/scanner.ts`
- `apps/web/src/lib/components/PendingSyndicationBanner.svelte`
- `apps/web/src/lib/blurt/ops/syndicateAck.ts`
- `apps/indexer/src/indexer/handlers/syndicateAck.ts`
- `apps/indexer/src/api/pendingSyndication.ts`
- ✅ Dispatcher entry for `morphit_syndicate_ack_v1` removed from OP_IDS
+ HANDLERS map
- ✅ Frontend `OP_IDS.syndicateAck` mirror removed from `$net/config`
- ✅ Frontend `getPendingSyndication` removed from indexer client;
`PendingSyndicationResponse` import dropped
- ✅ Indexer main.ts: pendingSyndication import + route mount removed
- ✅ Indexer orders API: `syndicate_opt_in` + `syndicated_trx_id`
dropped from `OrderRow`, `rowToWire`, and SELECT SQL
- ✅ Order handler: `syndicate_opt_in` dropped from ValidatedOrder
type, validate() parse, and all 3 INSERT paths (waived / BTC-XMR / BLURT)
- ✅ Shared types (`packages/indexer-client/src/index.ts`):
`OrderRecord.syndicate_opt_in`, `OrderRecord.syndicated_trx_id`,
and the entire `PendingSyndication*` section removed
- ✅ Order payload: `syndicate` field removed from `OrderPayload`,
`OrderFormInput`, and `buildOrderPayload`
- ✅ Layout: `PendingSyndicationBanner` mount + scanner onMount removed
-`/post` form:
- New `publish.ts` module with `publishFirstTradePost` and
`publishOrderPost` functions
- State variable renamed: `syndicate``syndicateToBlog`
- New checkbox copy: `syndicate.opt_in_label` / `opt_in_help`
- Review-step reminder block uses `syndicate.review_on_title/body`
- Post B wired at both success sites (waived/btc/xmr AND BLURT paths):
fire-and-forget `fireSyndicationPost(permlink)` when opted in
- Success card shows 3-state block for syndication:
pending / ok / failed
- ✅ i18n: 11 new `syndicate.*` keys localized across all 10 locales.
Parity clean at 1078 leaves.
- ✅ Common verbs added: `common.buying` / `common.selling` across
10 locales (used in Post B title and body).
## Post A wiring — **SHIPPED**
Previously blocked on the feedback-broadcast UI not existing. That UI
now exists; Post A is wired.
**New frontend pieces:**
- `apps/web/src/lib/blurt/ops/feedback.ts``broadcastFeedback` op
builder signed with posting key, parallel to profile.ts pattern.
Client-side validation mirrors the indexer handler's rules (subject
regex, self-review check, rating range, comment length + forbidden
chars, permlink format). Throws `FeedbackValidationError` with
indexer-aligned reason codes.
- `apps/web/src/lib/components/LeaveFeedbackForm.svelte` — inline
disclosure form. Fields: counterparty account (text), rating (1-5
star buttons), comment (optional textarea with live codepoint
counter). Broadcasts feedback via posting key (no password
prompt, no active key). On success, fires `publishFirstTradePost`
iff the `morphit.syndication.firstTradeFired.<account>`
localStorage flag is unset; sets the flag to avoid redundant
retries.
- `apps/web/src/routes/my/orders/+page.svelte` — new disclosure
slot in the live-order action column, between feature-bid and
cancel. Button "Mark complete / review" opens the inline form.
On submit-success the row shows "Feedback submitted. Thanks!"
**Permlink change**: `firstTradePermlink()` in publish.ts is now
account-keyed (`morphit-first-trade-<account>`) rather than
trx-keyed. This makes "once per account" a structural property
— duplicate broadcasts become chain-side edits rather than new
posts. Idempotent even without the localStorage flag; the flag
just avoids the redundant broadcast.
**i18n**: 16 new keys (feedback.form.*, feedback.error.*,
feedback.success_line, my_orders.order.action_feedback) localized
across all 10 locales. Parity clean at **1102 leaves**.
**Not covered yet** (future work, not blocking):
- Profile pages rendering received feedback
- Feedback-response op-builder + UI (subject replies to a review)
- Feedback-given list for viewing a user's own history
- "Is this your first feedback?" indexer endpoint (for multi-device
correctness; currently we rely on the account-keyed permlink and
a per-device localStorage flag)
## Dead schema columns (v11)
The v11 columns `orders.syndicate_opt_in` (BOOLEAN NOT NULL DEFAULT FALSE)
and `orders.syndicated_trx_id` (VARCHAR(64)) remain in the schema but
are no longer read or written by any code path. Existing rows keep
whatever values they had; new rows get the DEFAULT FALSE for opt-in
and NULL for trx_id.
**Action**: optional v12 migration to drop the columns. Low priority;
dead columns cost nothing in Postgres. Leave as-is until a general
schema cleanup turn.
## Phase 3 — force first-trade = BLURT BUY ($1 min) — **COMPLETE**
Shipped in the same resume session as Phase 1:
**Indexer** (`apps/indexer/src/indexer/handlers/order.ts`, waiver branch):
- ✅ Reject with `waiver_requires_blurt` when `asset !== 'BLURT'`
- ✅ Reject with `waiver_requires_min_usd` when `amount_min === null`
- ✅ Reject with `waiver_requires_min_usd` when
`amount_min < 1 / priceSource.current()` (i.e. less than $1 USD
worth of BLURT)
- Checks run fail-fast before the expensive prior-order lookup
- Price source is the same one the BLURT fee-verification path uses,
so there's no divergence between waiver gate and fee sizing
**Frontend** (`/post` page):
- ✅ Asset selector: BTC and XMR buttons are disabled (greyed,
`pointer-events-none` title hint) when `feeMethodChoice ==
'waived_first_buy'`
- ✅ Two-way sync in the waiver `$effect`:
- On waiver choice, auto-set asset to BLURT
- On user switching to non-BLURT, drop waiver back to paid BLURT
-`waiverMinBlurtForOneUsd` derived from `feeQuote.usdPerBlurt`
(the price fetch the fee-quote path already does)
-`amountError` extended: waiver path requires `amountMin !=
null` AND `amountMin >= floor`, error key
`post_order.errors.waiver_min_usd_required`
- ✅ Floor hint shown below amount inputs in waiver mode:
"Minimum for the waiver: {N} BLURT (~$1 USD at current price)."
**i18n**: 4 new keys added across all 10 locales
(`post_order.form.waiver_asset_locked_title`, `waiver_asset_hint`,
`waiver_min_hint`, `post_order.errors.waiver_min_usd_required`).
Parity clean at 1082 leaves.
## Locked constants
- @morphit community account: `blurt-176570`
- Community browse URL: `https://blurt.blog/created/blurt-176570`
- Morphit project account: `morphit`
- Relay account: `morphit-relay`
- Fees account: `morphit-fees`
- Image A (first-trade):
`https://img.blurt.blog/blurtimage/morphit/e3d56ddc849685c391dcdb03526463b8264f3e09.png`
- Local repo copy: `apps/web/static/brand/thumbnails/morphit-first-trade.png`
- Image B (order-post):
`https://img.blurt.blog/blurtimage/morphit/ed05997f374e75ed59746588f09c0771f136df26.png`
- Local repo copy: `apps/web/static/brand/thumbnails/morphit-trade.png`
- Morphit web: `https://morphit.io/`
- Blurt image host (not used for syndication but noted): `https://images.blurt.blog`
**Note on image references.** The syndicated posts embed the
on-chain `img.blurt.blog/blurtimage/morphit/...` URLs — those are
the canonical source for anything the chain sees. The local repo
copies in `apps/web/static/brand/thumbnails/` are operator backup
only, so the thumbnails survive if the Blurt image CDN URLs ever
change or go offline. They are not served from the app at
production traffic.
## Blurt specifics (unchanged from research)
- Community posts: `parent_permlink = <community-account-name>`.
Morphit community is `blurt-176570`.
- Personal blog posts: `parent_permlink = <primary-tag>`. We use
`"morphit"` so posts are discoverable under #morphit.
- Native `comment` op signed with the posting key. Both posts use
the existing `broadcastComment` helper in `$blurt/ops/comment`.
- Duplicate permlinks per author are treated as edits by the
chain, not as new posts — gives us free retry safety.
## Scope recap (unchanged)
Morphit supports trading any of **{BTC, XMR, BLURT}** with **any fiat**
(USD, EUR, GBP, MXN, JPY, etc.) via any agreed-upon payment method
(cash in person, PayPal, Western Union, bank transfer, etc.). Also
direct crypto-for-crypto pairs among {BTC, XMR, BLURT}.
No stablecoin pairs (no USDT, no USDC).