Social
Comments

Comments

A comment on Superhero is a post, written through the same contract and entrypoint and just as permanent. The only difference is a marker in the media list naming the parent. That one convention gives you arbitrarily deep threads with no threading support in the contract.

The convention

A comment is a post_without_tip call whose first media entry is the literal string comment: followed by the parent post's id, with its _v3 suffix:

title = "Great post!"
media = ["comment:3903_v3"]

Any further media entries after the marker are the comment's own attachments. The contract records the same tip_meta with the author, text and timestamp, and returns a new tip_id that is itself a valid parent for further replies.

Because the parent is a string in the post body, a comment is as permanent as any other post, and a parent can never be deleted, so a thread can never be orphaned.

How the indexer reads it

The indexer reads the call's second argument (the media list) and looks for an entry containing the literal comment:. It takes everything after that string as the parent id, trims it, and appends _v3 if it is not there already. An entry with comment: and nothing after it is stored as a top-level post.

Emit comment: plus the id with the _v3 suffix. That is what the app and the CLI write.

When the parent is missing

On ingest the indexer looks the parent up, retrying three times with exponential backoff from 100 ms, because backward sync does not guarantee that a parent is processed before its child. If the parent is still missing after the retries, the comment is stored as a standalone top-level post: post_id is set to null and it is ranked like any other post.

A second sweep catches the race the other way round. About ten seconds after boot, one UPDATE detaches every comment older than one hour whose parent still does not exist, and a second UPDATE recomputes total_comments for every parent whose count is stale, which also fixes parents that arrived after their children. The one-hour grace period means a comment whose parent is merely running behind in the sync is not detached.

A comment naming a parent that never existed therefore becomes an ordinary post. Nothing reverses that later.

Threading and depth

Nothing limits nesting. /post/:slug resolves the post, extracts its parent id, fetches that post, extracts its parent, and repeats, building the full ancestor chain oldest-first before rendering. The walk is bounded by a visited set and a limit of 100 hops.

The page renders ancestors above the target post and scrolls the target into the centre of the viewport. That is why a notification about a reply opens the comment, with its parents above it, rather than the parent post.

Direct replies are fetched separately: GET /api/posts/{id}/comments with order_direction set to ASC, 50 per page, draining a few pages automatically so short threads render fully. Each reply carries its own total_comments, so the UI can show a reply count without fetching the subtree.

Routes

RouteBehaviour
/post/:slugThe canonical post or comment page, with its ancestor chain
/post/:slug/comment/:idLegacy; redirects to /post/ plus the comment id

Post ids are stored with a _v3 suffix and every in-app link strips it. A link built from a raw API id still works because the resolver re-adds the suffix.

Read comments from the API

GET /api/posts/{id}/comments?order_direction=ASC&limit=50&page=1
ParameterValues
idThe parent post id, with the _v3 suffix
order_directionASC or DESC
limit, pageStandard pagination; the response carries items and meta

Reading needs no wallet and no signature.

From the command line

node scripts/superhero-comment.mjs post <post_id> "<content>"
node scripts/superhero-comment.mjs get <post_id>
node scripts/superhero-comment.mjs get <post_id> 50 2   # limit, page

post builds media as a single comment: entry and calls post_without_tip. It needs AE_PRIVATE_KEY and posts to mainnet. The output is JSON:

{
  "success": true,
  "comment_post_id": "3910",
  "parent_post_id": "3903",
  "content": "Great post!",
  "tx_hash": "th_..."
}

get is a plain read against https://api.superhero.com/api/posts/<id>/comments and needs no wallet. comment_post_id is the bare integer. Add _v3 before you use it as an API id or as a parent for a further reply.

A comment pays gas only, the same as a post.

Limits

The web composer applies the same 280-character limit to replies as to posts, and offers the same emoji, GIF and image attachments. The contract applies no limit of its own, and the indexer applies the same 5,000-character truncation and five-media-entry cap it applies to posts. See Posting.

The comment: marker is not a URL, so the indexer's media filter drops it from the stored media array. A comment's API media field contains only its own attachments, while tx_args still carries the marker. Do not look for the parent id in media on an API response.

How comments feed ranking

A comment is never ranked as a standalone popular item, because the candidate query requires post_id IS NULL. Instead the whole reply tree rolls up into the parent's score, counted recursively to a depth cap of 50, with replies written by the root post's own author excluded and hidden comments pruning their entire subtree. Comments carry the heaviest default weight of any signal, 2.5. See The Feed.

Related

  • Posting: the entrypoint, the ids, and the content conventions comments share.
  • The Feed: where comment counts feed into the popular ranking.
  • Notifications: a reply is the post-comment notification type.
  • The Tipping contract: the contract reference.
  • Posts API: the endpoint-level reference for posts and comments.