26 KiB
psf-memo — Feature Backlog
Status: DRAFT — refreshed 2026-09-03. Owner: specifier. Last updated: 2026-09-20
Goal
Make psf-memo feature-equivalent to memo.cash, a Bitcoin
Cash (BCH) social network built on OP_RETURN transactions. Every social action
is a BCH transaction carrying a Memo protocol payload (0x6d + action byte)
that is broadcast from the client to the chain and later indexed by
psf-memo-indexer into psf-memo-db.
Current direction
Core functionality is implemented and shipped. For the foreseeable future the
focus is front-end improvements to psf-memo-client (the React SPA).
- All previously listed roadmap features (P0–P6) have been removed from this backlog.
- New work should target the client: UI/UX polish, accessibility, performance, responsiveness, state handling, error surfacing, and any other front-end improvements.
- A single user-facing feature may still touch more than one component; call out all affected components in the task description and in the handoff.
In progress
- None.
Recently completed
-
Feed tabs (2026-09-20): the Following feed is merged into the
/posts/recentposts page as a row of two mode buttons ("Recent" / "Following"). On first load the page asksGET /follow/following/:addrand selects Following when the viewer's address follows at least one account, Recent when it follows no one; the viewer can switch tabs at any time and switching resets to the first page. The/posts/followingroute and its navbar item are removed, so the Following feed is reached only through the Following button. A new pureFeedTabsPageservice (psf-memo-client/src/services/feed-tabs-page.js) composes the existingRecentFeedPageandFollowingFeedPagecontrollers behind injectedmemoDb/wallet; the React shell reads the controllergetState()snapshot. Client-only read feature. Spec:psf-memo-client/specs/feed-tabs.feature. Merged tomasterat2d1755a03d(fast-forward; review commite4bda31256; recorddocs/reviews/feed-tabs-verification.json; the later2d1755acommit is docs-only, so the record is valid for the merged tree). Independent acceptance check after merge: feed-tabs 12/12 scenario examples. Soft Gherkin mutation 42/0 (all intrinsic example-value case substitutions); language mutation 22/22 killed; architect summarydocs/reviews/feed-tabs-summary.md. -
Following feed cap (2026-09-20):
GET /posts/following/:addrno longer full-scans the globalpostHeightsindex to computepagination.total(the live total reached 10942). The scan now stops afteroffset + limit + 500eligible followed posts and reportstotal = min(eligible, 500), and reply detection uses per-candidateisReplypoint lookups instead of iterating the wholepostParentsstore. The cap counts eligible followed posts, not raw index entries (followed posts are sparse). DB-only; the client renderspagination.totalunchanged, so the heading now reads "Showing 1–50 of 500". Spec:psf-memo-db/specs/following-feed-performance.feature. Merged tomasteratde3f1c8(review commitb7c2056; recorddocs/reviews/following-feed-cap-verification.json; the laterde3f1c8commit is docs-only, so the record is valid for the merged tree). Independent acceptance check after merge: db 3/3. Soft Gherkin mutation 17/14 (3 intrinsic survivors); language mutation 40/40 killed; architect summarydocs/reviews/following-feed-cap-summary.md. -
Mute broadcast result (2026-09-18): the profile page now shows a broadcast result modal after a mute (
0x6d16) or unmute (0x6d17) is broadcast. On success the modal shows a broadcast success message, the mute transaction id, and abch.loping.netblock-explorer link that opens in a new tab; on failure it shows the real broadcast error message. A successful mute still flips the button to Unmute and a successful unmute flips it back to Mute; a failed broadcast leaves the button unchanged. The modal stays open until dismissed, and dismissing it closes the modal without navigating. Pure result state lives inProfilePage(lastMuteResult,getMuteBroadcastMessage,getMuteResultError,dismissMuteResult); the presentationalMuteResultand sharedExplorerTxLinkcomponents are plainReact.createElement, shared with the Node acceptance adapter (acceptance/lib/render-mute-result.js). Client-only. Spec:psf-memo-client/specs/mute-broadcast-result.feature. Merged tomasterateaaed8e5ea(review commita7d9ca6299; recorddocs/reviews/mute-broadcast-result-verification.json; the latereaaed8ecommit is docs-only, so the record is valid for the merged tree). Independent acceptance check: client 4/4. Soft Gherkin mutation 5/5 intrinsic survivors; language mutation 31/31 killed. Architect summary:docs/reviews/mute-broadcast-result-summary.md. -
Notification entry display (2026-09-18): each
/notificationsentry now shows the actor's Memo display name and avatar instead of only the raw BCH address, resolved client-side from the name and profile-picture records. The avatar and display name link to/profile/<addr>, the full address renders as small non-emphasised plain text, and like/reply entries carry a "View Post" link that opens the referenced original post's thread modal. Follow entries have no post link. Fallbacks: no display name shows the truncated address, no avatar (or a failed profile lookup) shows an identicon. Client-only. Spec:psf-memo-client/specs/notification-entry-display.feature. Merged tomasterata1e4a4f95f(review commitbbbf4adcd7; recorddocs/reviews/notification-entry-display-verification.json; the latera1e4a4fcommit is docs-only, so the record is valid for the merged tree). Independent acceptance check: client 16/16. Soft Gherkin mutation 36/11 (all survivors intrinsic example-value case mutations); language mutation 20/20 killed. Architect summary:docs/reviews/notification-entry-display-summary.md. -
Notifications query performance (2026-09-18):
GET /posts/notifications/:addris now bounded to the viewer's activity inside a configurable block window (NOTIFICATION_BLOCK_WINDOW, default 25000; cutoffstatus.chainBlockHeight - window) instead of full-scanninglikes,postChildren, andfollows. The viewer's posts are read fromaddrPostHeightswithin the window, likes and replies are prefix-scanned frompostLikes/postChildrenper post, and follows come from a new followee-keyed, height-orderedfolloweeHeightsindex written by the indexer (/level/followeeheightroute,backfill-followee-indexutility).pagination.totalcounts only in-window notifications, and because a notification is drawn from the viewer's content inside the window, an interaction with a post older than the window is not returned even when the interaction itself is recent. Indexer + DB. Specs:psf-memo-indexer/specs/followee-heights-indexing.feature,psf-memo-db/specs/backfill-followee-index.feature,psf-memo-db/specs/notifications-query-performance.feature. Merged tomasterata1eb548688(review commitc9728cc301; recordsdocs/reviews/notifications-query-performance-verification.json(db) anddocs/reviews/notifications-query-performance-indexer-verification.json; the latera1eb548commit is docs-only, so the records are valid for the merged tree). Developer documentation captured inpsf-memo-indexer/dev-docs/psf-memo-db.md(newfolloweeHeightsstore,NOTIFICATION_BLOCK_WINDOWconfig, route notes) andpsf-memo-indexer/dev-docs/design-decisions-and-tradeoffs.md(why per-object scans plus a block window replace the full-store scans); architect summarydocs/reviews/notifications-query-performance-summary.md. Independent acceptance check after merge: db notifications 10/10, db backfill 4/4, indexer 4/4. -
Topics table layout (2026-09-18): the topics page now renders topics in a react-bootstrap
Table, so the four columns line up. A purebuildTopicsTableview model (psf-memo-client/src/services/topics-table.js) supplies the header labels (Topic,Most recent post,Posts,Followers), each row's cells in fixed order (#room, relative time,N posts,N followers), the topic-feed link, and thetable-responsivehorizontal-scroll wrapper; the ReactTopicscomponent is a thin shell over it. Client-only rendering. Spec:psf-memo-client/specs/topics-table-layout.feature. Merged tomasterat1e58b5b(review commitd1f0f76; recorddocs/reviews/topics-table-layout-verification.json). Independent acceptance check: client 5/5. Soft Gherkin mutation 16/16 killed; language mutation 2/2 killed. -
Topic metadata columns (2026-09-18): the
/topicspage now shows four columns — topic name, time since the most recent post, post count, and follower count. The indexer writeslastSeen(epoch-ms of the newest room post; 0 for follow-only rooms) andfollowerCount(active follows) into eachtopicSummariesrecord, idempotently and without disturbing the room's post metadata;GET /topicsreturns both; the room backfill rebuilds both fromrooms; and the client formatsNo posts/Less than an hour ago/N hours ago/N days agofromlastSeen. Client + indexer + DB. Specs:psf-memo-indexer/specs/topic-metadata-indexing.feature,psf-memo-db/specs/topic-metadata.feature,psf-memo-client/specs/topic-metadata-columns.feature. Merged tomasterataf54b0e(review commit3f05488; recordsdocs/reviews/topic-metadata-verification.json(indexer),-db-verification.json,-client-verification.json). Independent acceptance check: indexer 12/12, db 14/14, client 9/9. -
Topic recency ordering and pagination (2026-09-17):
GET /topicsnow orders topics by each room's most recent post and paginates without scanning the wholeroomsstore. The indexer maintains two new stores:topicSummaries(one record per room withpostCountandlastHeight) andtopicRecency(one record per room at its latest post height; follow-only rooms at height 0), both idempotent.GET /topicsgainedlimit/offsetand returnspagination; rooms with posts come first by most recent post height descending, ties by room name ascending, follow-only rooms last. A topic backfill utility (util/room/backfill-topic-indexes.js) builds both indexes from existingrooms. The client topics page loads 50 per page with Previous/Next. Client + indexer + DB. Specs:psf-memo-indexer/specs/topic-recency-indexing.feature,psf-memo-db/specs/backfill-topic-indexes.feature,psf-memo-db/specs/topic-pagination.feature,psf-memo-client/specs/topic-pagination.feature. Merged tomasterat090f41f(review commit0e8f8f0; recordsdocs/reviews/topic-recency-pagination-verification.json(indexer),-client-verification.json, and-db-verification.json). -
Memo multi-push encoding (2026-09-17): fixed the payload layout for every multi-field Memo action.
minimal-slp-wallet'ssendOpReturn(msg, prefix)can only emit two OP_RETURN pushes ([prefix, msg]), so the client had been concatenating all fields into one push. The indexer and memo.cash expect one push per protocol field, so the indexer loggedinvalid reply push data count 2and dropped every reply, topic message, add-poll-option, and poll vote; create-poll was accepted by our indexer only because it tolerates the combined form. Added the browser-safepsf-memo-client/src/services/memo-multipush.jsadapter (attachMultiPushOpReturn/broadcastMultiPush) and switched the five action services to broadcast field arrays: reply[6d03, txid(32 LE), text], topic message[6d0c, topic, text], add-poll-option[6d13, txid(32 LE), option], poll vote[6d14, txid(32 LE), comment], and create-poll[6d10, poll_type, option_count, question]. Client-only. Spec:psf-memo-client/specs/memo-multipush-encoding.feature. Merged tomasterat4dedc51(review commitfac0173; recorddocs/reviews/memo-multipush-encoding-verification.json, 425 unit / 85 property / 31 acceptance suites / lint / build). -
Txid wire encoding repair (2026-09-16): fixed the endianness bug that broke every like, reply, poll option, and poll vote broadcast by psf-memo-client. The client embedded the referenced txid in big-endian display order while the indexer expected little-endian wire order (
txHashFromPushreverses it), so the indexer stored a byte-reversed reference that never matched the post/poll: like counts read 0, replies vanished from threads, and poll options/votes detached.hex.jsnow ownstxidToWireBytes;buildTxidTextPayloadreverses the txid, and the reply-onlybuildReplyPayloadwas replaced by that shared helper. Addedpsf-memo-db/src/lib/repair-txid-encoding.js(correctReference/repairTxidEncoding) and thepsf-memo-db/util/txid/repair-txid-encoding.jsCLI to rewrite existing reversed references inlikes/postLikes,postParents/postChildren,pollOptions, andpollVotes, usingposts/pollsexistence to keep the correct orientation, leave unknown targets alone, and stay idempotent. The 20-byte hash160 follow/mute path is unchanged. Client + DB. Specs:psf-memo-client/specs/txid-wire-encoding.featureandpsf-memo-db/specs/repair-txid-encoding.feature. Merged tomasterat8f24ac0(review commita2229f7; recordsdocs/reviews/txid-wire-encoding-verification.jsonanddocs/reviews/txid-wire-encoding-db-verification.json). -
Like broadcast result modal (2026-09-16): after a successful like (with or without a tip), the like/tip modal no longer auto-closes. It now shows a broadcast success message, the like transaction id, and a block-explorer link to the transaction, mirroring the New Post result modal. The like count and filled heart still update, and the user must manually dismiss the result to close the modal. The explorer URL was extracted to a shared
psf-memo-client/src/services/block-explorer.jsused by the New Post modal, the post options menu, and the like result. Client-only behavior. Spec:psf-memo-client/specs/like-broadcast-result.feature. Merged tomasteratf92f596, tasklike-result-modal. -
Post options menu (2026-09-16): every post card now has a working three-dots "Post options" menu. The menu is hidden until the button is clicked; its first (top) item is "See on block explorer", linking to the post transaction at
https://bch.loping.net/tx/<txid>in a new tab. Clicking the button again, clicking outside the menu, or pressing Escape closes the menu, and ArrowDown moves focus to the first item. One sharedPostOptionsMenucomponent is used by the recent/following/topic feeds, the thread modal, and the profile post card; pure behavior lives inpsf-memo-client/src/services/post-options.js. Client-only rendering feature. Spec:psf-memo-client/specs/post-options-menu.feature. Merged tomasteratfdb6efc. -
Feed total cap raised to 500 (2026-09-16):
GET /posts/recentnow caps its total scan at 500 eligible top-level posts instead of 10 (TOTAL_SCAN_CAPinpsf-memo-db/src/adapters/post-query.js). Corpora with up to 500 eligible top-level posts report an exactpagination.total; larger corpora reporttotal = 500and read at mostoffset + limit + 500postHeights entries. Adds DB acceptance fixturemany-top-level-posts(510 posts) andrecent-feed-capproperty tests. DB-only behavior change. Specs:psf-memo-db/specs/feed-total-cap.featureandpsf-memo-db/specs/feed-query-performance.feature. Merged tomasterat5e62d1e. -
Post image rendering (2026-09-16): post text now renders inline images for URLs whose path ends in a common image extension (
.jpg,.jpeg,.png,.gif,.webp,.bmp; case-insensitive; query string and fragment ignored; SVG excluded). The image renders inside an anchor that opens the original URL in a new tab, with the URL's filename asalttext; the URL is not shown as text and surrounding text is preserved. Non-image URLs keep the plain-link behavior, and an image that fails to load falls back to a plain link. Pure helpersisImageUrl/imageAltTextinpsf-memo-client/src/services/post-links.js, a pure failed-image state transition insrc/services/failed-images.js, and a presentationalPostImage/PostContentrenderer. Client-only rendering feature. Spec:psf-memo-client/specs/post-image-rendering.feature. Merged tomasteratc2bfbc3. -
Post link formatting (2026-09-16): post text now auto-links
http://andhttps://URLs and bare domains such asmemo.fullstackcash.net. Explicit URLs keep their scheme; bare domains are linked withhttps://while their visible text stays as written. Links render as anchors withtarget="_blank"; embeddable YouTube links keep embedding instead of becoming plain links. Pure parserpsf-memo-client/src/services/post-links.js(parsePostLinks) feeds the sharedPostContentrenderer. Client-only rendering feature. Spec:psf-memo-client/specs/post-link-formatting.feature. Merged tomasteratb63792f. -
Thread query performance (2026-09-15):
GET /posts/:txid/threadnow does work proportional to the thread instead of the whole database. Like counts are computed only for the thread's txids viacountLikesForTxids(the globalbuildLikeCountMapwas removed), and child lookups prefix-scanpostChildrenper parent through a newPostQuery.listChildTxidsseam instead of walking the whole store once per node. Spec:psf-memo-db/specs/thread-query-performance.feature. Merged tomasteratc8ceb82. -
Account avatar display (2026-09-05): the
/accountpage now renders the avatar image when an avatar URL is set, instead of only showing the URL as text. A pureAvatarImagecomponent (src/components/account/avatar-image.js, plainReact.createElementso the browser build and the Node acceptance adapter share it) renders the<img>; theAccountPageservice exposesgetDisplayAvatarUrl/hasAvatarImage/getAvatarImageUrlas the testable seam. When no avatar URL is set, the page shows "No avatar URL set". Client-only rendering feature. Spec:psf-memo-client/specs/account-avatar-display.feature. Merged tomasterat5afaa64. -
Mute feed filtering (2026-09-04): muting a profile now hides that profile's content from the viewer's recent feed, topic feed, search results, and notifications. The psf-memo-db API filters server-side given the viewer's address (passed by the client as a
viewerquery param); filtering is not optimistic — a mute takes effect once the mute tx is indexed, and unmuting restores content once indexed. SharedloadMutedAddrs/isMutedPosthelper inpsf-memo-db/src/adapters/lib/muted-posts.jsdeduplicates the per-adapter lookup. Spec:psf-memo-client/specs/mute-feed-filtering.feature. Merged tomasterat3992395. -
Binary hash160 broadcast payloads (2026-09-04): client follow/unfollow and mute/unmute now broadcast the target's raw 20-byte hash160 as the OP_RETURN payload, built as a browser-safe
Uint8Arrayfrom thehexToByteshelper instead of Node'sBufferglobal (which crashed in a real browser withBuffer is not defined). Follow/mute/unfollow/unmute were consolidated onto a sharedMemoStateActionbase. Spec:psf-memo-client/specs/binary-payload-broadcast.feature. Merged tomasterat984e691. -
Page size 50 (2026-09-04): every paginated page in the client now requests 50 items per page instead of 100 to cut payload size and improve page load times. Covers the recent feed, following feed, topic feed, notifications, search, profile, and recent profiles pages. Pagination Previous/Next controls were also added to the search, profile, and recent-profiles pages (which previously had none), and the paginated page controllers were refactored onto a shared
PaginatedPagebase plusRecentProfilesPage. Spec:psf-memo-client/specs/page-size.feature. Merged tomasteratcfe6711. -
YouTube embed (2026-09-04): posts whose text contains a YouTube link (
youtube.com/watch?v=…oryoutu.be/…) render an embedded player instead of the raw URL; surrounding text is preserved; non-embeddable URLs stay plain text. Client-only rendering feature. Spec:psf-memo-client/specs/youtube-embed.feature. Merged tomasteratb63019c. -
Feed query performance (2026-09-05):
GET /posts/recentno longer does two full scans per request. Reply counts are computed per returned post (countRepliesForTxids) instead of a globalbuildReplyCountMap()scan, and thetotal/hasMorecomputation is a capped scan of the lastTOTAL_SCAN_CAP(10) top-level posts instead of walking the wholepostHeightsindex.list-recent-posts.jsnow usesscanRecentPostTxidsAndCount()which returns the page txids plus a capped total in one bounded scan. Spec:psf-memo-db/specs/feed-query-performance.feature. Merged tomasterat2bcc965.
Research notes
- Protocol reference:
https://memo.sv/protocol(Wayback Machine snapshot 2025-12-15). It lists action bytes, payload shapes, and byte limits. The page is on the BSV fork (memo.sv) but the action codes match the BCHmemo.cashimplementation. - memo.cash access: the live site is behind Cloudflare. Direct
curland headless Firefox login attempts from this environment were blocked, so the roadmap was derived from the protocol spec plus an audit of the existing mono-repo code.
Architecture constraints
- Identity/auth: the auto-generated HD wallet (12-word mnemonic) persisted in browser Local Storage by the existing React app is the Memo identity.
- Write path: broadcasting is done via
minimal-slp-wallet.sendOpReturn().- Correct public API:
await wallet.sendOpReturn(message, prefix, bchOutput). prefix = '6d02'posts a memo; other action bytes replace02.- Binary payloads (txid 32 bytes, address hash 20 bytes, topic/poll text) must be encoded correctly.
- Correct public API:
- Read path:
psf-memo-dbREST API (/posts/*,/profile/*,/level/*). API changes are in scope for specs. - Indexer path:
psf-memo-indexerscans blocks and mempool for MemoOP_RETURNoutputs and writes structured records topsf-memo-db. - The write path (broadcast), indexer path, and read path (DB) are asynchronous: a broadcasted action becomes visible only after confirmation + indexing.
Memo protocol action codes
Reference: https://memo.sv/protocol (Wayback snapshot 2025-12-15)
| Action byte | Meaning | Payload |
|---|---|---|
0x6d01 |
Set name | name (≤ 217 bytes) |
0x6d02 |
Post memo | message (≤ 217 bytes) |
0x6d03 |
Reply to memo | txhash (32 bytes) + message (≤ 184 bytes) |
0x6d04 |
Like / tip memo | txhash (32 bytes) |
0x6d05 |
Set profile text | message (≤ 217 bytes) |
0x6d06 |
Follow user | address (20 bytes) |
0x6d07 |
Unfollow user | address (20 bytes) |
0x6d0a |
Set profile picture | url (≤ 217 bytes) |
0x6d0b |
Repost memo | txhash (32 bytes) + message (≤ 184 bytes) — planned |
0x6d0c |
Post topic message | topic_name + message (combined ≤ 214 bytes) |
0x6d0d |
Topic follow | topic_name |
0x6d0e |
Topic unfollow | topic_name |
0x6d10 |
Create poll | poll_type (1) + option_count (1) + question (≤ 209 bytes) |
0x6d13 |
Add poll option | poll_txhash (32) + option (≤ 184 bytes) |
0x6d14 |
Poll vote | poll_txhash (32) + comment (≤ 184 bytes) |
0x6d16 |
Mute user | address (20 bytes) |
0x6d17 |
Unmute user | address (20 bytes) |
0x6d24 |
Send money | address (20) + message (≤ 194 bytes) |
0x6d30 |
Sell tokens | MIP-0009 token exchange |
0x6d31 |
Token buy offer | MIP-0009 token exchange |
0x6d32 |
Attach token sale signature | MIP-0009 token exchange |
0x6d35 |
Pin token post | MIP-0009 token exchange — planned |
Component legend
| Code | Component | Typical changes |
|---|---|---|
| C | psf-memo-client |
React components, services, pages, unit/acceptance tests |
| I | psf-memo-indexer |
Memo action handler, parser support, filter logic |
| D | psf-memo-db |
LevelDB store, REST route, query adapter, tests |
Process & tooling improvements
- 2026-09-16 process-efficiency pass: architect always notifies the specifier;
acceptance LevelDB temp dirs are cleaned up; the handoff daemon self-heals;
APS is single-sourced at
tmp/aps; acceptance generation is incremental;verify.shemits machine-readable verification records;mutate-file.shguards differential under-selection;clean-builds.shkeeps worker copies small; handoffs are blocked on a dirty tree. Seedocs/process-improvements.mdfor the what/why, commits, and verification evidence.
Next up: TBD
Current direction is front-end improvements to psf-memo-client (UI/UX polish,
accessibility, performance, responsiveness, state handling, error surfacing).
Ask the user for the next feature.
Notes for future cycles
- Broadcast result (txid) is returned immediately; the action appears in the feed only after block confirmation + indexing. Specs must reflect this async visibility.
- Mutations/specs are Gherkin feature files under per-component
specs/in the format defined by github.com/unclebob/Acceptance-Pipeline-Specification. - Root
specs/contains this backlog and cross-component architecture notes.