22 KiB
Morphit — Plan v1.3 (Locked)
Status: locked 2026-04-16. Changes require an ADR (Architecture Decision Record) in this directory.
2026-05-11 forward note (Part 120 audit): Plan v1.3 is "locked," but Accepted ADRs in
docs/adr/have since amended several specifics. Where this doc and an ADR conflict, the ADR wins. Specifically:
- Implementation language. This doc describes the relay and indexer as "Go service" (§"Order book," §"Phase 3a," §"Phase 3b"). Both shipped as Node.js/TypeScript services with
tsxas the runtime. See ARCHITECTURE.md for the current topology + ADR-0008 (indexer architecture).- Edit window. §"Immutability & fraud traps" already carries the 15-minute window (per ADR-0001's 2026-05-07 amendment) but Phase 3c §"Order posting + enforcement" still mentions "3-min replace-window." 15 minutes is authoritative. See
docs/adr/0001-custom-json-replacement.mdAmendments.- Nostr mirror. §"Order book" lists a "Secondary: Nostr mirror" path. This was never built; orderbook distribution is via Blurt + federation gossip + SSE streams. See ARCHITECTURE.md.
REST + RSS. Indexer exposes REST + SSE buses, not RSS feeds.- Payment-watcher. §"Origin decoupling" lists "payment- watcher SSE" as a runtime data endpoint. There is no separate payment-watcher service; fee verification runs inside the indexer process (
bitcoinExplorerVerifier.ts,moneroProofVerifier.ts). See ARCHITECTURE.md "Fee verification" subsection.account_create_with_delegationmechanism. §"Phase 3a" describes inline-pay account creation. Shipped usescreate_claimed_accountconsuming pre-minted ACTs per ADR-0010 §4.This file is preserved as the historical plan-of-record; ADRs in
docs/adr/and the runtime docs (ARCHITECTURE.md, OPERATIONS.md, FEES-AND-REWARDS.md) carry current truth.
Brand
- Wordmark:
apps/web/static/brand/morphit-wordmark.svg - Round mark:
apps/web/static/brand/morphit-mark.svg - Typeface: Nunito (SIL OFL) — self-hosted, subsetted
- Palette: gradient
#8EEF26 → #00DA69 → #02A6B2, accent#7FED2D
Reach
Clearnet (morphit.io), vanity v3 .onion, vanity .loki, vanity .b32.i2p. Same codebase, four endpoints, per-vhost CSP.
Core principles (non-negotiable)
- Non-custodial — operators never touch user trade funds
- No KYC — keypair = identity, no email / phone / name
- Grandma-friendly — 5-second comprehension rule
- Perfect Forward Secrecy wherever secrets exchanged
- Self-healing — multi-mirror, automatic failover
- FOSS core — indexer, relay, bot all open-source
- Keys never leave the device — client-side crypto only
Identity
- In-browser keygen (WebCrypto + libsodium)
- Mandatory backup (seed phrase + encrypted keyfile)
- Two paths: Build Reputation (persistent) or Maximum Anonymity (fresh key per trade, no reputation)
- Blurt account creation atomic with first listing fee
- BLURT-denominated invite vouchers
- No 2FA. No biometrics. Password-encrypted keystore only.
Display name (editable) vs. key identity (permanent)
- Key identity is the permanent, unspoofable anchor. Users are always
shown as
<display name> (BLT7gHu8mn…A9bb)— the truncated posting public key is the authoritative part, rendered in a monospace font next to the display name so it can't be forged or visually confused. - Display name is a human-readable label the user can change at any
time via a signed
morphit_profile_v1custom_jsonop. The op carries the new display name; the indexer treats the latest signed profile op as current. Older names remain on chain (auditable history). - Display names have no uniqueness guarantee — two users can both call themselves "Sally Doe"; the key disambiguates them. This is explicitly documented in the UI to prevent social-engineering confusion.
- Length: display name is capped at 40 characters, must be printable Unicode, cannot contain control characters, zero-width joiners, or bidirectional-override codepoints (all of which enable spoofing).
- No moderation, no registry. Morphit operators never curate or reserve names; doing so would mean operators ranking users, which contradicts the non-custodial principle.
Order book
- Primary: Blurt
custom_json, user-signed - Secondary: Nostr mirror (indexer-managed keypair, no user action)
- Posting relay: FOSS, pays RCs, broadcasts signed ops; multi-endpoint failover
- Indexer: FOSS Go service, uses public Blurt RPC pool
(
rpc.blurt.blog,blurt-rpc.saboin.com,rpc.beblurt.com) with rotation; exposes REST + RSS; gossips with peer indexers - No local Blurt full node in v1
Immutability & fraud traps
Important: custom_json ops are not mutable at the Blurt protocol level.
"Edits" in Morphit are implemented at layer 2 by posting a signed
morphit_order_replace_v1 op that references the original order's id.
Indexers treat the latest replacement (by on-chain timestamp, signed by the
same account) as canonical only if posted within the edit window.
Replacements posted after the window are ignored by conforming indexers.
Both the original and the replacement remain on chain forever, so history
is auditable.
- Orders: replaceable only while
openAND <15 minutes since post - Lock on
negotiatingregardless of clock (the first counterparty expression of interest closes the window) - Feedback: never replaceable (no
morphit_feedback_replace_v1op exists) - Chat: tamper-evident (each message AEAD-authenticated under a per-message key; sender-bound by the chain transaction signature)
- Anti-sybil fee escalation (per 24h per Blurt account):
- Orders 1–3: standard $0.25
- Orders 4–10: +25% each successive
- Orders 11+: +50% each
- Resets when old orders cancel / expire
- Self-trade detection: indexer correlates fee-payment patterns and account timing; flagged pairs' feedback weighted to zero, marked publicly
- Account age displayed prominently on profiles
Rationale for the 15-minute edit window
The original ADR-0001 chose 3 minutes specifically to close a bait-and-switch attack window: a seller could post favorable terms, wait for an interested buyer to DM, then silently replace the terms with worse ones during "negotiation." Three minutes was claimed to be long enough to fix a typo and short enough that no realistic trade conversation has begun.
In pre-launch Sally walkthroughs the 3-minute figure proved punishing: a user who steps away from the keyboard for ~4 minutes after posting and then notices a typo has no recovery except cancel-and-repost (paying another listing fee). The threat-model re-analysis in ADR-0001's 2026-05-07 amendment showed the bait-and-switch attack is bounded by two structural mitigations independent of window length: (a) trade-side commitment requires a separate Blurt broadcast and renders the order-version hash inline, so a switched listing is visible at commitment, (b) every replace leaves a permanent on-chain audit trail, so a switch-after-DM is forensically detectable and reputationally costly under the feedback system. The window was extended to 15 minutes for the typo-fix case while documenting the reduced (but still present) bait-and-switch asymmetry.
The full amendment lives in
docs/adr/0001-custom-json-replacement.md "Amendments →
2026-05-07."
Trade lifecycle
States: open → negotiating → in_progress → completed | cancelled | expired
- Default orderbook-visible: 14 days (user-adjustable 1–30)
- Default in-progress payment: 5 days (user-adjustable 1–14)
- Optional business-hours mode pauses payment timer on weekends / holidays
- One mutual extension per trade via countersigned op
Assets & fees
Cryptos (all pairs tradeable)
- BTC
- XMR
- BLURT
Fiat methods (20, fixed list)
- Cash in person
- Cash by mail
- National bank transfer
- SEPA (EU)
- SWIFT (international wire)
- PayPal
- Wise
- Revolut
- Zelle (US)
- Venmo (US)
- CashApp (US/UK)
- MoneyGram
- Western Union
- Bitso (MX / LatAm)
- Gift cards (Amazon, Steam, etc.)
- Mobile money (M-Pesa, etc.)
- Alipay (CN)
- WeChat Pay (CN)
- UnionPay (CN / Asia)
- Other — discuss in chat
Monetization
- Standard listing: $0.25 USD-equivalent
- BLURT discount: 50% off when fee paid in BLURT
- Featured bump: dynamic auction (floor-above-next), cheapest in BLURT
- BLURT/USD price: klingex.io primary, $0.002 hardcoded fallback, labeled
livevsfallbackin UI - No donations page, no paid support tier
Payment UX
- BLURT: ~6s spinner, instant feel
- BTC: 1 confirmation (~10 min); on-chain only
- XMR: 1 confirmation (~2 min); no zero-conf
- All flows: human-readable status, elapsed time, dismissible ("come back later"); notification on confirm
Receiving-address policy
When a user is asked to enter their own BTC or XMR receiving address (as part of an order where someone will be paying them):
- BTC: we accept P2PKH (1…), P2SH (3…), SegWit v0 (bc1…), and Taproot (bc1p…). All mainnet formats are treated equally by validation; the UI surfaces the detected type as a small badge. Testnet, signet, and regtest addresses are rejected.
- BTC — fresh-address recommendation: the client warns (soft — does
not block) if the user enters an address they've used in a past order.
Reuse tracking is local-only: SHA-256 hashes of past addresses live in
localStorage, keyed by currency. The rationale is shown inline via a
Tooltip linked to the
why_fresh_addressesFAQ entry. - XMR: subaddresses only (prefix
8, length 95 chars, mainnet). Standard addresses (prefix4, length 95) are rejected with a specific error message explaining that subaddresses are Monero's modern privacy default. Integrated addresses (prefix4, length 106) are also rejected — they pre-date subaddresses and have been superseded. This is a hard block, not a soft warning, because the privacy delta is large and the UX cost of pasting a subaddress is zero. - Rotating placeholder text: the address input cycles through 3 short nudges in the active locale ("Paste a fresh BTC address" → "Use a new address for each trade" → "Never reuse addresses — it hurts your privacy"), pausing while the field is focused or typed into.
- On-blur validation: invalid addresses show a locale-specific error inline with a red border; reused addresses show an amber soft warning; valid addresses show a green border and a small badge indicating the type (Legacy / P2SH / SegWit / Taproot / Subaddress).
- No address phoned home. Validation is pure-TS, runs entirely in the browser, and never touches the network. Reuse memory lives in localStorage as SHA-256 hashes, never as plaintext addresses.
Chat (private, persistent, partial PFS)
- X25519 + ChaCha20-Poly1305 ECIES (libsodium); see ADR-0015
- Per-message sender ephemerals (one-sided forward secrecy)
- Long-term chat identity derived from posting key (BLAKE2b)
- Ciphertext on Blurt, decryption local only
- Full decrypted history available to participants
- AEAD MAC makes tampering detectable
- NO per-message receiver-key rotation (deliberate; full rationale in ADR-0015 and FAQ)
Feedback & reputation
- Signed feedback op on Blurt per trade, includes
role: buyer | seller - Profile displays: total trades, positive/negative by role, account age, first-seen, response-time avg, BLURT stake (opt-in)
- Response to feedback supported, never editable after posting
- Self-trade flagged pairs weighted to zero
Escrow
- none — ever. Morphit is a pure bulletin board with reputation as safety. We will never offer escrow, multisig, or arbitration of any kind. Reintroducing custody at any layer would reintroduce the middleman the project exists to remove.
Assets & media
- Icons (flags, currency, method): bundled SVGs, SVGO-optimized, zero network
- Avatars: ordered URL list + SHA-256 hash in Blurt profile; client verifies hash, falls back through list, then to deterministic identicon
- SVG avatar "pro mode": power users store tiny SVG directly in
custom_json
Unstoppability
- Fully static frontend (SvelteKit SSG)
- Multi-mirror by default, automatic failover
- PWA + service worker cache (installed app survives host outages)
- IPFS release mirror (IPNS address)
- Signed source-tarball distribution for self-hosting operators (the PWA install covers end users — no separate APK / Flatpak)
- Blurt
custom_jsonrelease-discovery op (metadata only — version + IPFS CID + mirror list) - RSS consumable independent of frontend
- Two-way indexer gossip
- Four parallel network transports
Origin decoupling (the installed PWA)
Once a Morphit client is installed as a PWA from any instance, it never
depends on morphit.io again:
- Every static asset (HTML, JS, CSS, fonts, icons, locale bundles) is precached by the service worker on install. The running app has zero origin dependency for rendering.
- Data endpoints (indexer REST / RSS, relay, payment-watcher SSE) come
from an in-app configurable endpoint list, seeded with several
community-run indexers and refreshable from the Blurt release-discovery
op.
morphit.iomay be one of many entries and is not privileged. - Update policy is pin-on-install. A new release is fetched and precached into a new versioned cache, but the running app continues to serve from the current one until the user consents to switch. This prevents a compromised edge host from silently replacing the bundle.
The design implication: morphit.io is a distribution convenience, not a
runtime dependency — once installed, the PWA runs entirely from its own
precached assets and the in-app configurable endpoint list, so any instance
(or none) can serve the next launch.
No-JS & performance
- No-JS read-only mode: browse orders, profiles, feedback (SSG HTML)
- JS required for trading (client-side crypto only, never escapes browser)
- Zero third-party JS, zero analytics, zero tracking, zero cookies, no logs
- Optional Privacy Mode: sessionStorage only, service worker off
- Budget:
- <100KB gzipped JS for trading paths
- <20KB gzipped CSS
- Nunito subset ~25KB/weight
- CJK loaded on demand for Mandarin / Cantonese
- WebP / AVIF with PNG fallback, lazy-loaded
- Brotli + gzip pre-compressed static assets
- Low-data mode toggle (no avatars, batched requests)
- Tested on simulated 2G + old Android WebView
FAQ
- Searchable with auto-complete, multilingual (all 10 languages)
- Tooltips across the UI deep-link to matching FAQ entries, multilingual
- Mandatory topics:
- What is Morphit?
- Is it safe?
- What do I need to sign up?
- How do I buy / sell crypto?
- What are the fees? (incl. BLURT discount)
- Order timeouts (14-day default, 5-day payment, 3-min replace lock, weekend pause option)
- Order editing (3-min replace window via layer-2 replacement op, locked on negotiation, chain keeps full history of both ops)
- Chat privacy (E2E encrypted via per-message ECIES; sender-side forward secrecy; never decryptable by Morphit)
- Feedback immutability (no edits, response allowed)
- What data does Morphit collect? (none)
- Lost keys / password (unrecoverable — non-custody tradeoff)
- What is BLURT? (+ link to klingex.io)
- Troubleshooting
- Who runs this?
Languages (at launch)
English (default), Spanish, German, Polish, French, Italian, Russian, Persian, Mandarin, Cantonese. JSON bundles, lazy-loaded per language, instant live switch.
Support
- Matrix bot bridges in-app widget ↔ private Matrix room
- Admin replies from any Matrix client (Element, Cinny, FluffyChat, etc.)
- Zabbix monitors infra separately, alerts to Matrix
Tech stack (locked)
- Frontend: SvelteKit (SSG), Tailwind CSS, svelte-i18n
- Backend services (Go, single-binary):
morphit-indexer— Blurt/Nostr → PostgreSQL → REST + RSSmorphit-relay— signed-op broadcastmorphit-payment-watcher— BTC/XMR/BLURT address monitoringmorphit-avatar-server— static + upload + resize
- Support bot: Python
- Database: PostgreSQL 16
- Crypto libs: libsodium (chat AEAD + keystore), blurt-js, bitcoinjs-lib, monero-js (address validation only — never custody)
- Reverse proxy: nginx (four vhosts: clearnet + Tor + Lokinet + I2P)
- Hidden-service daemons: Tor (v3), Lokinet, i2pd
- CI: Forgejo Actions (lint + test + reproducible build)
VPS
- 4 cores / 8 GB RAM / 80 GB SSD / Ubuntu 24.04 LTS
- WireGuard mesh for inter-service comms
- Unprivileged systemd units per service, each in its own directory
- Only 80/443 + hidden-service ports exposed publicly
- fail2ban + crowdsec on edge, no IP logging anywhere
- Nightly encrypted PostgreSQL dumps
- Reproducible builds where possible
Questions for VPS provider
- Does my VPS have a dedicated public IPv4, or am I behind CGNAT / NAT?
- Are any inbound ports blocked by default? I need 80, 443, and custom TCP/UDP ports.
- Do you support reverse DNS (PTR) changes?
- Is IPv6 provisioned with a routed subnet?
Answers (Agorise primary operator, confirmed 2026-04-20)
- Dedicated public IPv4 — Yes. No CGNAT; inbound reachability is unobstructed. Clearnet users can connect directly; Tor/Lokinet/I2P hidden services reverse-proxy cleanly to local sockets.
- Inbound port policy — No defaults blocked. 80, 443, and the custom TCP/UDP ports needed for the relay and indexer will land without provider-side intervention.
- Reverse DNS (PTR) — Yes, operator can set PTR to match the forward record. Useful for legitimacy signals on any outbound connection and for future email (operator notifications) if we ever add it.
- Routed IPv6 subnet — Yes. Operator can assign distinct v6 addresses per service if useful; IPv6-first futures are available.
No deployment blockers. Proceed from docs/OPERATIONS.md.
Vanity hidden-network addresses
Generated on operator hardware, keys never transmitted:
- Tor v3
morphit…:mkp224o - Lokinet
morphit.loki:lokinet-vanity - I2P
morphit…b32.i2p: i2pd /eepgen
Development phases
- Phase 1 (complete): repo, SvelteKit shell, Nunito design system, i18n (10 languages), keygen + backup, searchable FAQ, no-JS read-only paths, CSP / SRI / reproducible-build scaffolding
- Phase 2 (complete): BIP-39 mnemonic, session identity store, endpoint-rotation client, Blurt chain read + sign client, profile-op broadcast, onboarding confirm-seed step, update banner, Italian / Russian / Persian locales (Persian adds first-class RTL support), ISO-pill language switcher, lazy-loaded locale bundles, FAQ share links, SEO foundations, provider-swappable price feeds with hardcoded fallback + "prices updated X ago" UI indicator (ADR-0004)
- Phase 3 — Relay, indexer, orderbook (complete). Split into three subphases,
each shipping a standalone tarball so progress is testable as it
lands rather than at the end:
- 3a: Posting relay + account creation. Go
relay service (
apps/relay/) that accepts owner-signedaccount_create_with_delegationops from clients and pays the Blurt RC cost. Client-side account-registration UI that checks name availability, collects a chosen account name, signs locally with the user's owner key viauseOwnerKey(), hands the signed op to the relay, and stores the confirmed account name on success. First real on-chain flow: a new user onboards and ends up with a Blurt account without ever touching a third-party service. Display-name broadcast (Phase 2'smorphit_profile_v1path) now works end-to-end because themorphitaccount exists and posts discovery ops the client can read. - 3b: Indexer + orderbook read. Go indexer service
(
apps/indexer/) that streams the Blurt chain, keeps a Postgres table ofmorphit_order_v1/morphit_order_replace_v1/morphit_order_cancel_v1ops, exposes REST + RSS endpoints with the full filter set promised in therss_feedsFAQ entry (pair / side / location / method / min_rep / min_age / max_deviation), and gossips with peer indexers. SvelteKit orderbook route reads live data and renders filterable results. Users can browse real offers even before posting is wired up. Includes a self-hosting guide (docs/self-hosting/indexer.md) per Phase 1 carry-forward #11. - 3c: Order posting + enforcement. Client-side order compose
UI,
morphit_order_v1+morphit_order_replace_v1ops, 3-min replace-window enforcement at the indexer (ADR-0001), sybil fee logic (escalating-per-24h), self-trade detection, optional featured-slot auction. JITuseActiveKey()wired into the BLURT fee path. Users can post an order and someone else can see it. End of this subphase: Morphit has a functional, minimum-viable P2P orderbook visible across multiple community-run indexer instances.
- 3a: Posting relay + account creation. Go
relay service (
- Phase 4 (complete): encrypted chat, feedback with roles, reputation display, attack-surface log
- Phase 5 (complete): payment flow (BTC / XMR / BLURT), featured-bump auction, payment watcher, Matrix bot, Zabbix monitoring, Tor / Lokinet / I2P vhosts, IPFS release pipeline, federated-instance onboarding (PWA install — no separate APK / Flatpak packaging needed), klingex research, WhaleVault / Gravity browser-extension signing path
- Phase 6 — API integrations & marketing (in progress): a scoped, embeddable orderbook screen developers can drop into their own UI, so users can search / browse / respond to offers without leaving an interface they already trust; plus the public launch and marketing push
Operator action items (outside this repo)
Completed (as of Phase 3 kickoff):
- ✓ Register
morphit+morphit-relayon Blurt — keys stay with operator - ✓ Create Matrix admin account +
#agorise:matrix.orgcommunity room - ✓ Generate BTC / XMR receiving wallets
Still outstanding:
- SSH access to Forgejo (Phase 3 workflow improvement; patch handoff continues via tarballs until this is resolved)
- VPS provider questions (Plan §342 — noisy-neighbor, IP history, Tor exit-node policy, censorship-takedown history)
Change control
Changes to this plan require a numbered ADR in docs/adr/NNNN-title.md,
referencing which section of Plan v1.3 is being amended and why.