# Micro-SaaS & Side Project Ideas Finder 🚀 (`joey001/microsaas-sideproject-finder`) Actor

Find side project ideas and micro-SaaS opportunities from Hacker News Show HN, ranked by momentum - points gained per hour - so you see what is gaining traction now. Includes upvotes, comments, velocity and an engagement tier. No API key needed.

- **URL**: https://apify.com/joey001/microsaas-sideproject-finder.md
- **Developed by:** [Joe Y](https://apify.com/joey001) (community)
- **Categories:** Lead generation, Developer tools, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 launch founds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Micro-SaaS & Side Project Ideas Finder

Find side project ideas and micro-SaaS opportunities, ranked by how fast they are climbing
right now.

This Actor scans Hacker News **Show HN**, keeps the launches that point at an actual
product, demo or repository, and ranks them by **momentum** — points gained per hour —
so the table leads with projects that are taking off today rather than ones that slowly
accumulated a big total months ago. Every row also carries the lifetime totals, the
comment velocity, and a debate-versus-approval ratio, so you can re-rank it any way you
need.

**No API key, no credentials, no setup.** Show HN is read through Algolia's public
Hacker News Search API.

### Why Show HN

Show HN is where makers post their projects, and by convention every submission links to
something real that you can open and try. That makes it a much better source for
launch discovery than a general discussion forum:

- Every post is a launch, not a question or an opinion piece.
- Each one carries points and comment counts, so engagement is directly comparable.
- The archive goes back over a decade, with roughly 1.95 million launches indexed.
- No authentication, so the Actor runs immediately and never breaks on expiring tokens.

### Input

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `timeFilter` | string | `month` | How far back to look, based on submission date: `day`, `week`, `month`, `year` or `all`. Longer windows make any given row count more selective — see [the window matters](#the-window-matters-as-much-as-the-row-count). |
| `maxPosts` | integer | `100` | Maximum number of matching launches to return (10–200). If fewer match, you get all of them. See [How many results you get](#how-many-results-you-get). |
| `sortBy` | string | `velocity` | Ranking field: `velocity` (points per hour), `engagement` (lifetime upvotes + comments), `comments` (lifetime comment count) or `newest`. See [Choosing a sort](#choosing-a-sort). |
| `onlyWithExternalUrl` | boolean | `true` | Keep only launches that link off Hacker News. Drops text-only threads and self-links back to `news.ycombinator.com`, so `projectUrl` always points at a real destination. |

### Output

Each run fills the default dataset with one row per launch, sorted by `engagementScore`
descending.

| Field | Type | Description |
| --- | --- | --- |
| `title` | string | Title of the post, including its `Show HN:` prefix. |
| `projectUrl` | string | Where the launch points: the product, demo, landing page or repository. Empty for text-only posts if you disable the filter. |
| `projectDomain` | string | Hostname of `projectUrl`, useful for grouping posts about the same product. |
| `postPermalink` | string | Permanent link to the original Hacker News discussion. |
| `author` | string | Hacker News username of the poster. |
| `upvotes` | integer | Hacker News points at the time of the run. |
| `numComments` | integer | Number of comments on the post. |
| `engagementScore` | integer | `upvotes + (comments × 2)`. Lifetime engagement. Nearly redundant with `upvotes` (ρ = 0.96), so prefer `pointsPerHour` for ranking. |
| `hoursOld` | number | How many hours before the run the post was submitted. `0` if no timestamp was available. |
| `pointsPerHour` | number | Upvotes ÷ age in hours. The default ranking field. |
| `commentsPerHour` | number | Comments ÷ age in hours. |
| `discussionRatio` | number | Comments ÷ upvotes. High values mean a launch provoked debate rather than passive approval. `0` when a post has no upvotes. |
| `engagementTier` | string | Velocity label from `pointsPerHour`: `viral` (≥3), `hot` (≥1), `rising` (≥0.25), `steady` (below 0.25). |
| `createdAt` | string | ISO 8601 UTC timestamp of when the post was submitted. |

#### Choosing a sort

The `sortBy` input picks the ranking field. Measured across 300 real Show HN launches,
these options are **not** interchangeable — Spearman rank correlation:

| sort | correlated with `engagement` by | median age of its top 8 |
| --- | --- | --- |
| `velocity` | 0.73 | **31 hours** |
| `engagement` | 1.00 | 468 hours (~3 weeks) |
| `newest` | — | — |

**`velocity` (default)** surfaces what is climbing now. Because every rate is divided by
age, it can rank a 145-point launch from this morning above a 554-point launch from last
week — which is usually what you want when hunting for emerging projects. Its top ranks
are not noise: the top 8 all had 85+ points.

**`engagement`** re-ranks by lifetime totals, giving you the proven winners. Be aware that
this is almost identical to sorting by raw upvotes: across 300 launches, `engagementScore`
correlates with `upvotes` at **0.96**, so the comment weighting barely changes the order.
If you want a genuinely different second view, use `velocity`.

**`comments`** ranks by lifetime comment count, useful for finding launches that sparked
discussion rather than quiet approval.

**`newest`** is pure recency, for monitoring rather than ranking.

`discussionRatio` is deliberately **not** a sort option. It is uncorrelated with traction
(ρ = 0.05 against velocity), and its top ranks are dead posts with many comments and
almost no upvotes — a median of just 15 points. It is useful as a flag on good rows, never
as a ranking.

##### How the sort interacts with `maxPosts`

The Actor collects the **top `maxPosts` launches by points** first, then ranks that set by
`sortBy`. So `sortBy` reorders the results; it does not change which launches are fetched.
`maxPosts` therefore acts as both a result cap and an evidence floor.

This is deliberate. Ranking every recent submission by velocity would surface noise —
measuring the full past week by date turns up posts like *2 points at 30 minutes old*,
which computes to 4 points/hour and outranks a genuine 209-point launch. Requiring a
launch to earn its place on total points first filters those out.

The practical consequence: a brand-new launch with very few points will not appear, no
matter how steep its early curve. That is intentional — a 6-point post two hours old is not
a lead, and the tier logic refuses to label it one. If you genuinely want to catch launches
at that stage you would need to rank by date rather than points, which trades away the
quality filtering entirely.

#### Engagement tiers

`engagementTier` labels each launch by velocity, so you can filter the dataset to just the
fast movers. The thresholds are a heuristic, not a published Hacker News standard. They were calibrated
against 300 real Show HN launches, where points-per-hour has a median of **0.13** and a
maximum near **16** — so a "fast" launch moves only a few points per hour. At the default
`maxPosts` of 100 this puts roughly **12%** of launches in `viral`, **15%** in `hot` and
**30–50%** in `rising`.

Because rates fall as posts age, an `all time` run is legitimately mostly `steady`: an old
post is historically significant, not currently trending. Use a recent `timeFilter` if you
want the tier to discriminate.

**A launch needs at least 10 points before it can be labelled anything but `steady`.** Rate
metrics are meaningless on tiny numerators: a post submitted two hours ago with 6 points
computes to 3 points/hour and would otherwise be called `viral`, which the evidence does
not support. The floor matters most when a query reaches deep into low-scoring submissions;
at the default `month` window with 100 rows the lowest-scoring row has 62 points, so it
rarely binds.

The thresholds are defined as `ENGAGEMENT_TIERS` and the floor as `MIN_POINTS_FOR_TIER` in
the source if you want to reinterpret them.

#### Example output

```json
{
    "title": "Show HN: An e-ink frame that hears birds and draws them",
    "projectUrl": "https://github.com/arnegiacomo/fugleramme",
    "projectDomain": "github.com",
    "postPermalink": "https://news.ycombinator.com/item?id=42380493",
    "author": "arnegiacomo",
    "upvotes": 2391,
    "numComments": 267,
    "engagementScore": 2925,
    "hoursOld": 155.4,
    "pointsPerHour": 15.39,
    "commentsPerHour": 1.72,
    "discussionRatio": 0.112,
    "engagementTier": "rising",
    "createdAt": "2024-12-11T08:41:07+00:00"
}
```

### How many results you get

**If fewer launches match than you asked for, you get all of them.** Measured against an
independent count of the source data:

| `timeFilter` | Matching launches that exist | `maxPosts` | Returned |
| --- | --- | --- | --- |
| `day` | 155 | 200 | **155** |
| `week` | 951 | 200 | 200 |
| `month` | 984 | 200 | 200 |

Hacker News' search index exposes at most 1000 results per query, so no run can ever return
more than that — an `all time` run reaches 1000 of the 1.95 million Show HN posts that
exist.

#### `maxPosts` is a quality dial, not just a quantity dial

Results arrive from the API **already ranked by points, highest first**. `maxPosts` is
where you stop reading down that list, so every extra row is lower-scoring than the last:

| position | 1 | 10 | 50 | 100 | 200 | 400 | 975 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| points | 2391 | 392 | 116 | **62** | 16 | 6 | 3 |

The same 975-item list, cut in three places:

| `maxPosts` | Rows | Points range |
| --- | --- | --- |
| 100 | 100 | 2391 → **62** |
| 500 | 500 | 2391 → 5 |
| 1000 | 975 | 2391 → 3 |

So asking for 1000 does not get you 10× the value of 100 — it gets you the same top 100 plus
900 rows of noise.

#### The window matters as much as the row count

Drawing from a longer window makes any given row count *more* selective:

| rows | week window | month window |
| --- | --- | --- |
| 50 | 16 pts | 116 pts |
| **100** | **6 pts** | **62 pts** |
| 200 | 4 pts | 16 pts |
| 300 | 3 pts | 9 pts |

Note that `week` + 100 rows is **worse** than `month` + 100 rows — a week simply doesn't hold
enough submissions for the top 100 to be selective.

**The defaults are tuned for this:** `timeFilter: month` with `maxPosts: 100` gives a
62-point floor, so every row is a launch that earned real attention. `maxPosts` is capped at
200, which is roughly where the month-window floor drops to 16 points.

Raise `maxPosts` only if you want breadth and intend to filter the results yourself.

### How it works

1. Queries the Show HN search API, translating `timeFilter` into a submission-date cutoff
   and requesting results best-first.
2. Pages through the results until `maxPosts` matches are collected, honouring the
   external-URL filter.
3. Computes `engagementScore` for every post, plus age-normalised velocity metrics that
   distinguish a fast-rising launch from one that accumulated the same total slowly.
4. Sorts the whole result set once by the field you chose in `sortBy`, then writes it to
   the dataset.

Because it uses a lightweight JSON API rather than a headless browser, a run finishes in
seconds and stays cheap. A polite pause between pages keeps the Actor well inside the
API's rate limits. If the run is stopped early, everything collected so far is still saved
to the dataset.

### Cost

The Actor never launches a browser, and the default run makes only one or two HTTP
requests. Memory and CPU use are minimal, so you pay for just the few seconds of Apify
compute the run actually uses.

### Troubleshooting

**The run finished with 0 results** — every post was filtered out. Try `timeFilter: all`, a
larger `maxPosts`, or turn off `onlyWithExternalUrl` to include text-only threads.

**Fewer results than `maxPosts`** — the time filter or the external-URL filter removed
more posts than were available in the listing. Widen the time filter or raise `maxPosts`.

### Legal

This Actor reads Hacker News data through Algolia's public HN Search API. Usernames are
included in the output so you can identify who launched a project; treat the data
accordingly and respect the API's rate limits, which this Actor enforces by pausing
between requests.

# Actor input Schema

## `timeFilter` (type: `string`):

How far back to look, based on submission date. This is a quality dial as much as a recency one: results arrive ranked by points, so drawing from a longer window makes any given number of rows more selective. "Past month" is the default because it balances fresh leads against enough volume to filter out low-scoring submissions. "All time" returns historically significant posts rather than current leads.

## `maxPosts` (type: `integer`):

Maximum number of matching launches to return (10-200). If fewer match, you get all of them. Results are returned highest-scoring first, so this is where you draw the line down that list: at 100 rows on a month window every row has 60+ points, while going deeper pulls in submissions that never got attention. Raise it only if you want breadth and will filter yourself.

## `sortBy` (type: `string`):

Velocity ranks by points gained per hour, so it surfaces launches that are climbing right now and does not favour older posts that accumulated points slowly. Engagement ranks by lifetime upvotes + (comments x 2), which is close to ranking by raw upvotes. Comments ranks by lifetime comment count, and Newest by submission time.

## `onlyWithExternalUrl` (type: `boolean`):

Keep only launches that link to an off-Hacker-News destination. Drops text-only threads and self-links back to news.ycombinator.com, so the projectUrl column always points at a real product, demo or repository.

## Actor input object example

```json
{
  "timeFilter": "month",
  "maxPosts": 100,
  "sortBy": "velocity",
  "onlyWithExternalUrl": true
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("joey001/microsaas-sideproject-finder").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("joey001/microsaas-sideproject-finder").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call joey001/microsaas-sideproject-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,joey001/microsaas-sideproject-finder"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/yX6Z1zIt3H3W8R6GW/builds/MfQF2ytlAMvO9CUax/openapi.json
