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
| Route | Behaviour |
|---|---|
/post/:slug | The canonical post or comment page, with its ancestor chain |
/post/:slug/comment/:id | Legacy; 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| Parameter | Values |
|---|---|
id | The parent post id, with the _v3 suffix |
order_direction | ASC or DESC |
limit, page | Standard 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, pagepost 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-commentnotification type. - The Tipping contract: the contract reference.
- Posts API: the endpoint-level reference for posts and comments.