# Indie Hackers Products Scraper (`axiomworks/indie-hackers-products-scraper`) Actor

Scrape public Indie Hackers product pages and get names, taglines, descriptions, websites, self-reported monthly revenue, post counts, founders, Twitter links and logos. Pick specific products by URL or slug, or read the first page of the products directory.

- **URL**: https://apify.com/axiomworks/indie-hackers-products-scraper.md
- **Developed by:** [Axiom Works](https://apify.com/axiomworks) (community)
- **Categories:** Lead generation
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.79 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Indie Hackers Products Scraper

**Returns:** one row per Indie Hackers product with name, tagline, websiteUrl, monthlyRevenueUsd, postCount, founders, sourceUrl.
**Input:** `{"productSlugs": ["keep-the-score", "sql-pad"], "maxItems": 2}`
**Best for:** bootstrapped-product lead lists, competitor and niche research, revenue tracking over time.

```json
{
  "id": "keep-the-score",
  "sourceUrl": "https://www.indiehackers.com/product/keep-the-score",
  "name": "Keepthescore.com",
  "tagline": "Create digital sports scoreboards and streaming overlays",
  "websiteUrl": "https://Keepthescore.com",
  "monthlyRevenueUsd": 25000,
  "postCount": 5
}
```

### What does Indie Hackers Products Scraper do?

Indie Hackers Products Scraper collects public product pages from the Indie Hackers directory and returns them as clean, structured records. Each record describes one product: its name, tagline, about text, website, self-reported monthly revenue, number of posts, the people listed on it, its Twitter link and its logo. You can point the Actor at specific product URLs or pull the top of the directory in a chosen sorting.

The Actor reads only pages that anyone can open in a browser, without an account. It starts at most one request per second with two products processed at a time, including retries. Missing pages (HTTP 404 or 410) are skipped immediately. Temporary failures and blocked responses get up to two retries with backoff on fresh sessions. Each fetch has a 35-second retry budget and a 12-second attempt timeout. If no result arrives within about 65 seconds, discovery stops and the run succeeds with a message suggesting targets or proxy settings. Apify Proxy is available for blocked requests on the platform.

It is built for founders researching competitors and niches, investors and analysts who want a list of bootstrapped products with revenue figures, newsletter and directory builders, lead researchers who need a product's website and founder profile, and anyone who feeds product data into a spreadsheet, a database or an AI workflow.

Typical uses include:

- building a list of products that report a given monthly revenue or more;
- re-running the same product URLs to compare revenue and post counts over time;
- collecting product websites and Twitter links for outreach research;
- grouping products by category to see which niches are crowded.

An optional enrichment step assigns each product a category from a fixed list. That step uses a third-party AI processor, which receives the product name, tagline and description. You can switch it off with the AI category option.

### What data can you get?

Each product becomes one dataset item. Items are unique by `id` (the product slug), and `sourceUrl` points to the Indie Hackers product page the data came from.

| Field | Type | Description |
|---|---|---|
| `id` | string | Product slug, unique per record |
| `sourceUrl` | string | Product page the data came from |
| `name` | string | Product name from the page header |
| `tagline` | string | One-line tagline |
| `description` | string | About text from the sidebar |
| `websiteUrl` | string | Product website, repaired when the stored value is malformed |
| `monthlyRevenueText` | string | Self-reported revenue exactly as displayed |
| `monthlyRevenueUsd` | number | The same revenue parsed to US dollars per month, or null for suspicious values |
| `revenueSuspicious` | boolean | True when the revenue text has an unknown unit or an implausibly large parsed value; `MM` is treated as millions |
| `postCount` | number | Number of posts shown for the product |
| `founders` | array | People listed on the product: `name`, `profileUrl`, `role` |
| `twitterUrl` | string | Twitter or X link when the product lists one |
| `logoUrl` | string | Logo image URL |
| `scrapedAt` | string | ISO timestamp of when the product page was fetched |
| `category` | string | Category assigned by the AI step, empty when enrichment is off or failed |

Revenue is entered by the product owners themselves, so it is not verified. Some products show no revenue at all, and a few show nonsense values. For those the Actor keeps the original text, sets `revenueSuspicious` to true, clears `monthlyRevenueUsd`, and still returns the product.

### How to use Indie Hackers Products Scraper

1. Open the Actor in Apify Console and go to the Input tab.
2. Choose what to scrape. Paste product URLs into Product or listing URLs, or leave that field empty to read the directory.
3. Set Max items to the number of products you want.
4. Optionally set a minimum monthly revenue, and decide whether to keep the AI category.
5. Click Start. When the run finishes, open the Output tab to view the table or download JSON, CSV or Excel.

There are two ways to choose products:

1. **Specific products.** Put one or more `https://www.indiehackers.com/product/<slug>` URLs in Product or listing URLs. You can also put the `https://www.indiehackers.com/products` URL there, optionally with `?sorting=` set to one of the four sortings.
2. **Directory listing.** Leave the URLs empty. The Actor reads the first page of the products directory in your chosen sorting (about 18 products) and opens each product page for the full details. The directory does not expose further pages to a plain request, so a listing run returns at most one page of products.

If a run returns no rows, check the product URL or slug first. Remove `keyword` or lower `minMonthlyRevenue` to check whether filters excluded the products. A target that remains unavailable or blocked contributes no row; the final status suggests checking targets or enabling proxy. A run with no accessible products still finishes successfully with zero records. The directory exposes only its first page, so supply explicit product URLs to inspect other products.

### Input

| Field | Type | Description |
|---|---|---|
| `startUrls` | array | Product page URLs or the products listing URL. Overrides sorting when set. |
| `productSlugs` | array | Product slugs (or product URLs) such as `keep-the-score`. Added to `startUrls`. |
| `maxMonthlyRevenue` | integer | Inclusive monthly revenue ceiling; omitted by default. Combined with the minimum to select a revenue band. Missing and suspicious revenue are excluded when a bound is set. |
| `excludeSuspiciousRevenue` | boolean | Skip products flagged by `revenueSuspicious`, default false. Missing revenue is kept unless a revenue bound is set. |
| `keyword` | string | Keep only products whose name, tagline or description contains this text (case-insensitive). |
| `sorting` | string | `highest-revenue`, `lowest-revenue`, `recently-added` or `recently-updated`. Used when no product URL or slug is supplied. |
| `minMonthlyRevenue` | integer | Keep only products with at least this monthly revenue in US dollars. Products without revenue and suspicious values are dropped. |
| `maxItems` | integer | Maximum products to return, default 50. |
| `enableAiEnrichment` | boolean | Add the AI category, default true. |
| `proxyConfiguration` | object | Proxy settings. Direct by default, with an automatic datacenter proxy retry when blocked. |

Example input for the newest products in the directory:

```json
{
  "sorting": "recently-added",
  "maxItems": 15,
  "enableAiEnrichment": true
}
```

Example input for specific products by slug:

```json
{
  "productSlugs": ["keep-the-score", "sql-pad"],
  "maxItems": 2
}
```

Revenue filters run after the product detail request and before enrichment or saving. For example, `{"minMonthlyRevenue":1000,"maxMonthlyRevenue":30000}` keeps only products with parsed monthly revenue within that inclusive range. A ceiling without a minimum works too. These filters apply equally to explicit URLs, slugs, and directory candidates. They do not request another directory page when candidates are excluded. If the minimum exceeds the maximum, no products match; widen the range to collect results.

Set `excludeSuspiciousRevenue` to true to remove implausible claims while retaining products that have not reported revenue. This differs from setting a revenue bound, which also removes missing revenue. The flag uses the existing parser and its `revenueSuspicious` output; it does not verify the owner’s claim or establish that a plausible amount is accurate. Both options are additive and leave existing calls unchanged when omitted. Neither adds requests to a product fetch, and filtering happens before the optional AI classification.

### Output

This is a real item from a run with the AI category switched off. It is a product that reports no revenue, which is common among new listings.

```json
{
  "id": "pingtopay",
  "sourceUrl": "https://www.indiehackers.com/product/pingtopay",
  "name": "PingToPay",
  "tagline": "Automated payment reminders for freelancers and agencies",
  "description": "I'm a solo builder, and this came out of a problem I actually had: I freelance-adjacent work with clients abroad, and by the time I remembered an invoice was overdue, it was the middle of their night (or mine), and I'd s",
  "websiteUrl": "https://pingtopay.app",
  "monthlyRevenueText": null,
  "monthlyRevenueUsd": null,
  "revenueSuspicious": null,
  "postCount": 1,
  "founders": [
    {
      "name": "ResponsibleEye",
      "profileUrl": "https://www.indiehackers.com/ResponsibleEye",
      "role": "Founder"
    }
  ],
  "twitterUrl": null,
  "logoUrl": "https://storage.googleapis.com/indie-hackers.appspot.com/product-avatars/pingtopay/200x200_pingtopay.webp",
  "category": null
}
```

Fields the page does not provide are returned as `null` rather than invented. The Twitter link in particular is present for only a minority of products. In test runs, the name, tagline, description, website, post count, founders and logo were filled for every product, while revenue was missing for a small number of them.

You can download the dataset in JSON, CSV, Excel, XML or HTML, or read it through the Apify API.

### How much does it cost?

The Actor is charged per result: you pay for each product record saved to the dataset. Products that are skipped (not found, filtered out by the revenue filter, or duplicates) are not charged. When the AI category is switched on and succeeds for a product, that product also counts as an enriched result. If the AI step fails or is unavailable, the product is saved without a category and without the enrichment charge.

Current prices are on the Pricing tab of the Actor page, which is always up to date. The Max items setting is a hard cap, so a run never saves, and never charges for, more products than you asked for. Start with a small number to see the output before you scale up.

### Use with AI agents (MCP)

You can call the Actor from any MCP-compatible assistant through the Apify MCP server. Add this to your client configuration:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=axiomworks/indie-hackers-products-scraper"
    }
  }
}
```

Example prompts you can give an assistant that has the Actor connected:

- "Get the 15 most recently added products on Indie Hackers and list their taglines and websites."
- "Scrape these Indie Hackers product slugs and show the products that report at least 5,000 dollars of monthly revenue."
- "Scrape these three Indie Hackers product pages and tell me who the founders are."
- "Find Indie Hackers products with a Twitter link and group them by category."

The input schema lists the required and optional fields, so an agent can fill in the call without extra guidance.

### Integrations and API

You can run the Actor from code, schedule it, or connect it to other tools with the Apify integrations such as webhooks, Make, Zapier and Google Sheets.

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("axiomworks/indie-hackers-products-scraper").call(
    run_input={"sorting": "recently-added", "maxItems": 15}
)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["name"], item["monthlyRevenueText"])
```

**JavaScript**

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('axiomworks/indie-hackers-products-scraper').call({
    productSlugs: ['keep-the-score', 'sql-pad'],
    minMonthlyRevenue: 1000,
    maxItems: 2,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.info(items.length);
```

**cURL**

```bash
curl -X POST "https://api.apify.com/v2/acts/axiomworks~indie-hackers-products-scraper/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sorting": "recently-added", "maxItems": 15}'
```

### FAQ

**Why does a listing run return only about 18 products?**
The directory page serves its first page of products and ignores paging parameters when fetched without a browser session. To collect other products, supply their product URLs or slugs.

**Why is the revenue missing or odd for some products?**
Revenue is typed in by each product owner and is not checked. Some products leave it empty and some enter implausibly large values. The Actor keeps the original text in `monthlyRevenueText`, recognizes `K`, `M`, `MM` and `B`, and sets `revenueSuspicious` for unknown units or implausibly large parsed values while leaving `monthlyRevenueUsd` null.

**What does the minimum monthly revenue filter do?**
It is applied after each product page is read. Products with no revenue, with a suspicious value, or with a value below your limit are skipped. Because the Actor must open each page to check, a strict filter can return fewer products than Max items.

**Why is the website sometimes slightly different from the original text?**
Some owners store malformed links such as a single slash after the scheme or a bare domain. The Actor repairs these into valid web addresses and drops values that cannot be repaired.

**What happens with a wrong URL or a product that no longer exists?**
The Actor logs a warning for that URL, skips it and carries on. If nothing at all can be collected, the run finishes with zero items and a warning, with a status message suggesting what to change.

**Do I need a proxy?**
Usually not. Requests go direct by default and the Actor retries through an Apify datacenter proxy if it is blocked. You can select your own proxy group in the input.

**Is the category always present?**
No. It is added only when the AI step is on and succeeds. If it is unavailable, the product is still saved with an empty category.

**Can I get the full post history of a product?**
No. This Actor returns the product profile and the post count, not the individual posts.

### Is it legal to scrape Indie Hackers?

The Actor collects only information that is publicly visible without logging in, at a modest request rate. It does not access private data, bypass logins or solve CAPTCHAs. Even so, whether a particular use of the data is allowed depends on your jurisdiction and purpose. The records include the names and public profile links of people listed on products, which can count as personal data under privacy laws such as GDPR. Use that data for legitimate purposes, respect opt-out requests, and review the terms of the website and the laws that apply to you. This Actor is an independent tool and is not affiliated with or endorsed by Indie Hackers. This section is general information, not legal advice.

### Feedback

If a field is missing, a product page layout changes or you need another data point, open an issue from the Actor page in Apify Console. Include the input you used and the product URL involved, which makes problems much quicker to reproduce and fix.

# Actor input Schema

## `startUrls` (type: `array`):

Indie Hackers URLs as objects or strings, e.g. \[{"url":"https://www.indiehackers.com/product/sql-pad"}]. Accepts /product/{slug} (including subpages) or /products?sorting=recently-added. Combined with productSlugs; explicit targets replace automatic directory discovery. A directory URL uses its sorting query, otherwise sorting. Dead targets produce no row; check the slug or try another product.

## `productSlugs` (type: `array`):

Product slugs or full product URLs, e.g. \["sql-pad", "https://www.indiehackers.com/product/code-story"]. Combined with startUrls and deduplicated by slug. Empty by default; when both target lists are empty, scrape the first directory page using sorting. maxItems is shared across both lists.

## `sorting` (type: `string`):

Directory order when no explicit product targets are supplied, or for a /products URL without a sorting query. Example: recently-added. Default highest-revenue; the directory exposes only its first page (about 18 products), so maxItems cannot create more results.

## `minMonthlyRevenue` (type: `integer`):

Minimum self-reported monthly revenue as a nonnegative integer in USD, e.g. 1000. Omit for no revenue filter. Applied after fetching product details; excludes missing or suspicious revenue. If no rows match, lower this threshold or omit it; it does not discover additional directory pages.

## `keyword` (type: `string`):

Case-insensitive substring to match in the product name, tagline or description, e.g. saas. Omit or use an empty string for no text filter. Filters fetched targets; this is not a site-wide search. If no rows match, remove it or supply other product URLs.

## `maxItems` (type: `integer`):

Maximum saved products across all startUrls and productSlugs together, 1-10000, default 50; e.g. 3. Stops when reached. Filters, missing pages and the directory first-page limit can yield fewer rows. Use explicit product URLs or slugs to collect beyond one directory page.

## `enableAiEnrichment` (type: `boolean`):

Whether to classify each product with a third-party AI processor, e.g. false to skip classification. Default true. Uses name, tagline and description; category stays null if disabled or classification is unavailable. Other extracted fields do not depend on classification.

## `proxyConfiguration` (type: `object`):

Proxy object, e.g. {"useApifyProxy":false} (default, direct requests). Choose {"useApifyProxy":true} to use Apify Proxy. Blocked requests retry with fresh proxy sessions where available. If a target remains blocked, try enabling proxy or another product URL.

## `maxMonthlyRevenue` (type: `integer`):

Maximum self-reported monthly revenue in USD, inclusive. Omit for no ceiling. Combined with minMonthlyRevenue to select a revenue band; missing and suspicious revenue are excluded when either bound is set. Filters fetched products, without expanding directory discovery.

## `excludeSuspiciousRevenue` (type: `boolean`):

Skip products whose revenueSuspicious flag is true, such as implausibly large or unknown-unit claims. Default false preserves all existing results. Missing revenue remains eligible unless a revenue bound is set. Skipped products are not saved or charged.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://www.indiehackers.com/product/keep-the-score"
    },
    {
      "url": "https://www.indiehackers.com/product/sql-pad"
    },
    {
      "url": "https://www.indiehackers.com/product/code-story"
    }
  ],
  "productSlugs": [],
  "sorting": "highest-revenue",
  "maxItems": 3,
  "enableAiEnrichment": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "excludeSuspiciousRevenue": false
}
```

# Actor output Schema

## `dataset` (type: `string`):

One record per Indie Hackers product.

# 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 = {
    "startUrls": [
        {
            "url": "https://www.indiehackers.com/product/keep-the-score"
        },
        {
            "url": "https://www.indiehackers.com/product/sql-pad"
        },
        {
            "url": "https://www.indiehackers.com/product/code-story"
        }
    ],
    "maxItems": 3,
    "enableAiEnrichment": false,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("axiomworks/indie-hackers-products-scraper").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 = {
    "startUrls": [
        { "url": "https://www.indiehackers.com/product/keep-the-score" },
        { "url": "https://www.indiehackers.com/product/sql-pad" },
        { "url": "https://www.indiehackers.com/product/code-story" },
    ],
    "maxItems": 3,
    "enableAiEnrichment": False,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("axiomworks/indie-hackers-products-scraper").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 '{
  "startUrls": [
    {
      "url": "https://www.indiehackers.com/product/keep-the-score"
    },
    {
      "url": "https://www.indiehackers.com/product/sql-pad"
    },
    {
      "url": "https://www.indiehackers.com/product/code-story"
    }
  ],
  "maxItems": 3,
  "enableAiEnrichment": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call axiomworks/indie-hackers-products-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,axiomworks/indie-hackers-products-scraper"
        }
    }
}
```

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/DVE9R3vag1pt9jMqT/builds/qWvbx0bUOUX11G7nk/openapi.json
