Hacker News
Overview
Tech news aggregator by Y Combinator. Reads via Algolia Search API and Firebase API (node-direct). Writes via browser page context.
Workflows
Browse and read a story
getTopStories → pick story → note objectID
getStoryDetail(id) → title, url, points, author, nested comment tree
Upvote a story
getTopStories → pick story → objectID
upvoteStory(id=objectID) → {ok, id} (requires login)
unvoteStory(id=${prev.upvoteStory.id}) → {ok, id} — reverses the upvote (only valid while upvoted)
Comment on a story
getStoryDetail(id) → item.id
addComment(parent=item.id, text) → {ok, parent, id} — id is the new comment's id (requires login)
deleteComment(id=${prev.addComment.id}) → {ok, id} — must be within HN's ~2-hour delete window
Reply to a comment
getStoryDetail(id) → children[] → pick comment → comment.id
addComment(parent=comment.id, text) → {ok, parent, id} — posts reply (requires login)
Explore a user
getUserProfile(id) → karma, created, about
getUserSubmissions(id) → stories they posted
getUserComments(id) → their comment history
Find stories from a domain
getStoriesByDomain(query) → all stories linking to that domain
Read latest activity
getNewComments → newest comments across all stories
Operations
| Operation |
Intent |
Key Input |
Key Output |
Transport |
| getTopStories |
browse top stories |
— |
objectID, title, url, author, points, num_comments |
L1 node (Algolia) |
| getNewestStories |
browse newest |
— |
same as above |
L1 node (Algolia) |
| getBestStories |
all-time highest-voted |
— |
same as above |
L1 node (Algolia) |
| getAskStories |
recent Ask HN |
— |
same as above |
L1 node (Algolia) |
| getShowStories |
recent Show HN |
— |
same as above |
L1 node (Algolia) |
| getJobPostings |
browse jobs |
— |
objectID, title, url, created_at |
L1 node (Algolia) |
| getFrontPageStories |
time-based front page |
— |
same as feeds |
L1 node (Algolia) |
| getStoryDetail |
story + comment tree |
id (item ID) |
id, title, url, points, children[] |
L1 node (Algolia) |
| getUserProfile |
user profile |
id (username) |
id, karma, created, about |
L1 node (Firebase) |
| getNewComments |
newest comments |
— |
objectID, author, comment_text, story_title |
L1 node (Algolia) |
| getStoryComments |
comment thread |
id, limit? |
storyId, commentCount, comments[] |
adapter (Algolia) |
| getStoriesByDomain |
recent domain stories |
query (domain) |
objectID, title, url, author, points |
L1 node (Algolia) |
| getUserSubmissions |
user's stories |
id (username) |
objectID, title, url, author, points |
adapter (Algolia) |
| getUserComments |
user's comments |
id (username) |
objectID, author, comment_text |
adapter (Algolia) |
| upvoteStory |
upvote item |
id <- feeds/getStoryDetail |
ok, id |
adapter (page) |
| unvoteStory |
reverse upvote |
id <- upvoteStory |
ok, id |
adapter (page) |
| addComment |
post comment |
parent <- getStoryDetail, text |
ok, parent, id |
adapter (page) |
| deleteComment |
delete own comment |
id <- addComment |
ok, id |
adapter (page); ~2-hour window |
Raw Algolia wire shape
Read operations hit https://hn.algolia.com/api/v1/search over the node transport. The raw Algolia response is:
{
"hits": [ /* ... */ ],
"nbHits": 1234,
"hitsPerPage": 20,
"page": 0,
"nbPages": 50,
"processingTimeMS": 3,
"query": "",
"params": "tags=story"
}
The spec declares unwrap: hits, so adapters/agents receive just the hits array — the envelope (nbHits, page, etc.) is stripped by the runtime.
Each hit carries Algolia-indexed fields:
objectID — story/comment id as a string (cast if you need a number)
title, url, author, points, num_comments
story_text, comment_text — HTML strings (see note below)
created_at (ISO), created_at_i (unix seconds)
_tags — e.g. ["story", "author_pg", "story_12345"]
story_id, parent_id — for comments
Templated reads
Some reads template the id param into an Algolia filter/tag expression:
getStoryComments → numericFilters=story_id={id}
getUserSubmissions → tags=story,author_{id}
getUserComments → tags=comment,author_{id}
Because id is used as a template source, the runtime does not emit it as a bare query key — only the interpolated filter/tag appears on the wire.
HTML in text fields
comment_text and story_text are HTML fragments (typically wrapped in <p> or <pre>). When rendering, strip those tags (and decode entities) rather than displaying raw markup.
Quick Start
# Browse top stories (node-direct, no browser needed)
openweb hackernews exec getTopStories '{}'
# Get story detail with full comment tree
openweb hackernews exec getStoryDetail '{"id": 42407357}'
# Get comments for a story (with limit)
openweb hackernews exec getStoryComments '{"id": 42407357, "limit": 10}'
# Look up a user (Firebase API)
openweb hackernews exec getUserProfile '{"id": "pg"}'
# User's submitted stories
openweb hackernews exec getUserSubmissions '{"id": "pg"}'
# Stories from a domain
openweb hackernews exec getStoriesByDomain '{"query": "github.com"}'
# Latest comments site-wide
openweb hackernews exec getNewComments '{}'
# Upvote a story (requires browser + login)
openweb hackernews exec upvoteStory '{"id": 42407357}'
# Reverse the upvote (only valid while currently upvoted)
openweb hackernews exec unvoteStory '{"id": 42407357}'
# Comment on a story (requires browser + login)
openweb hackernews exec addComment '{"parent": 42407357, "text": "Great article!"}'
# Delete your own comment (HN ~2-hour delete window)
openweb hackernews exec deleteComment '{"id": 47830121}'
1---2name: hackernews3description: Hacker News4---5# Hacker News67## Overview8Tech news aggregator by Y Combinator. Reads via Algolia Search API and Firebase API (node-direct). Writes via browser page context.910## Workflows1112### Browse and read a story131. `getTopStories` → pick story → note `objectID`142. `getStoryDetail(id)` → title, url, points, author, nested comment tree1516### Upvote a story171. `getTopStories` → pick story → `objectID`182. `upvoteStory(id=objectID)` → `{ok, id}` (requires login)193. `unvoteStory(id=${prev.upvoteStory.id})` → `{ok, id}` — reverses the upvote (only valid while upvoted)2021### Comment on a story221. `getStoryDetail(id)` → `item.id`232. `addComment(parent=item.id, text)` → `{ok, parent, id}` — `id` is the new comment's id (requires login)243. `deleteComment(id=${prev.addComment.id})` → `{ok, id}` — must be within HN's ~2-hour delete window2526### Reply to a comment271. `getStoryDetail(id)` → `children[]` → pick comment → `comment.id`282. `addComment(parent=comment.id, text)` → `{ok, parent, id}` — posts reply (requires login)2930### Explore a user311. `getUserProfile(id)` → karma, created, about322. `getUserSubmissions(id)` → stories they posted333. `getUserComments(id)` → their comment history3435### Find stories from a domain361. `getStoriesByDomain(query)` → all stories linking to that domain3738### Read latest activity391. `getNewComments` → newest comments across all stories4041## Operations4243| Operation | Intent | Key Input | Key Output | Transport |44|-----------|--------|-----------|------------|-----------|45| getTopStories | browse top stories | — | objectID, title, url, author, points, num_comments | L1 node (Algolia) |46| getNewestStories | browse newest | — | same as above | L1 node (Algolia) |47| getBestStories | all-time highest-voted | — | same as above | L1 node (Algolia) |48| getAskStories | recent Ask HN | — | same as above | L1 node (Algolia) |49| getShowStories | recent Show HN | — | same as above | L1 node (Algolia) |50| getJobPostings | browse jobs | — | objectID, title, url, created_at | L1 node (Algolia) |51| getFrontPageStories | time-based front page | — | same as feeds | L1 node (Algolia) |52| getStoryDetail | story + comment tree | id (item ID) | id, title, url, points, children[] | L1 node (Algolia) |53| getUserProfile | user profile | id (username) | id, karma, created, about | L1 node (Firebase) |54| getNewComments | newest comments | — | objectID, author, comment_text, story_title | L1 node (Algolia) |55| getStoryComments | comment thread | id, limit? | storyId, commentCount, comments[] | adapter (Algolia) |56| getStoriesByDomain | recent domain stories | query (domain) | objectID, title, url, author, points | L1 node (Algolia) |57| getUserSubmissions | user's stories | id (username) | objectID, title, url, author, points | adapter (Algolia) |58| getUserComments | user's comments | id (username) | objectID, author, comment_text | adapter (Algolia) |59| upvoteStory | upvote item | id <- feeds/getStoryDetail | ok, id | adapter (page) |60| unvoteStory | reverse upvote | id <- upvoteStory | ok, id | adapter (page) |61| addComment | post comment | parent <- getStoryDetail, text | ok, parent, id | adapter (page) |62| deleteComment | delete own comment | id <- addComment | ok, id | adapter (page); ~2-hour window |6364## Raw Algolia wire shape6566Read operations hit `https://hn.algolia.com/api/v1/search` over the node transport. The raw Algolia response is:6768```json69{70 "hits": [ /* ... */ ],71 "nbHits": 1234,72 "hitsPerPage": 20,73 "page": 0,74 "nbPages": 50,75 "processingTimeMS": 3,76 "query": "",77 "params": "tags=story"78}79```8081The spec declares `unwrap: hits`, so adapters/agents receive just the `hits` array — the envelope (`nbHits`, `page`, etc.) is stripped by the runtime.8283Each hit carries Algolia-indexed fields:8485- `objectID` — story/comment id as a **string** (cast if you need a number)86- `title`, `url`, `author`, `points`, `num_comments`87- `story_text`, `comment_text` — HTML strings (see note below)88- `created_at` (ISO), `created_at_i` (unix seconds)89- `_tags` — e.g. `["story", "author_pg", "story_12345"]`90- `story_id`, `parent_id` — for comments9192### Templated reads9394Some reads template the `id` param into an Algolia filter/tag expression:9596- `getStoryComments` → `numericFilters=story_id={id}`97- `getUserSubmissions` → `tags=story,author_{id}`98- `getUserComments` → `tags=comment,author_{id}`99100Because `id` is used as a template source, the runtime does **not** emit it as a bare query key — only the interpolated filter/tag appears on the wire.101102### HTML in text fields103104`comment_text` and `story_text` are HTML fragments (typically wrapped in `<p>` or `<pre>`). When rendering, strip those tags (and decode entities) rather than displaying raw markup.105106## Quick Start107108```bash109# Browse top stories (node-direct, no browser needed)110openweb hackernews exec getTopStories '{}'111112# Get story detail with full comment tree113openweb hackernews exec getStoryDetail '{"id": 42407357}'114115# Get comments for a story (with limit)116openweb hackernews exec getStoryComments '{"id": 42407357, "limit": 10}'117118# Look up a user (Firebase API)119openweb hackernews exec getUserProfile '{"id": "pg"}'120121# User's submitted stories122openweb hackernews exec getUserSubmissions '{"id": "pg"}'123124# Stories from a domain125openweb hackernews exec getStoriesByDomain '{"query": "github.com"}'126127# Latest comments site-wide128openweb hackernews exec getNewComments '{}'129130# Upvote a story (requires browser + login)131openweb hackernews exec upvoteStory '{"id": 42407357}'132133# Reverse the upvote (only valid while currently upvoted)134openweb hackernews exec unvoteStory '{"id": 42407357}'135136# Comment on a story (requires browser + login)137openweb hackernews exec addComment '{"parent": 42407357, "text": "Great article!"}'138139# Delete your own comment (HN ~2-hour delete window)140openweb hackernews exec deleteComment '{"id": 47830121}'141```