# Sammyboy Forum Scraper (`lexac94/sammyboy-scraper`) Actor

Board and thread scraper for SG Forum Sammyboy.

- **URL**: https://apify.com/lexac94/sammyboy-scraper.md
- **Developed by:** [Lexa N](https://apify.com/lexac94) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 thread listings

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Sammyboy Forum Scraper

Scrapes **Sammyboy.com** (Sam's Alfresco Coffee) — one of Singapore's longest-running
discussion forums — into clean, structured data.

Two modes. Pick one row per thread for cheap, scheduled monitoring, or the full
contents of a thread with every reply. Keyword and date filtering, bulk input, and
**quoted text separated from what the author actually wrote**.

***

### What you get

#### 📋 Board threads mode

One row per thread, read straight off a board's listing pages. Never opens a thread,
so it's fast and cheap — good for daily monitoring of what's being discussed.

```json
{
  "post_id": "276666",
  "site": "sammyboy",
  "board": "17",
  "title": "Living in JB 4",
  "author": "wuqi256",
  "reply_count": "1K",
  "view_count": "282K",
  "created_at": "2019-11-21T12:13:05+0800",
  "last_reply_at": "2026-07-29T13:22:33+0800",
  "post_url": "https://www.sammyboy.com/threads/276666/",
  "content": null,
  "is_pinned": true,
  "source_mode": "forum",
  "source_query": "17",
  "scraped_at": "2026-09-14T12:30:35.798Z"
}
```

#### 💬 Full thread mode

The thread's opening post, plus one row per reply — **newest first**.

```json
{
  "reply_id": "4230332",
  "post_id": "111952",
  "site": "sammyboy",
  "floor": 1744,
  "author": "kezgtree",
  "content": "sori, to ask.. why need to reno roof when it's new or any other reasons?",
  "quoted_authors": ["liamricci"],
  "quote_text": "We are going to renovate our house this summer-autumn. Thanks for sharing your experiences, I appreciate it! I guess we'll start with the roof.",
  "created_at": "2025-04-21T00:29:35+0800",
  "scraped_at": "2026-09-14T12:27:13.169Z"
}
```

***

### Quoted text is separated, not mixed in

Forum replies quote each other constantly. Most scrapers flatten the whole post to
text, so the person you quoted ends up inside *your* comment — and every sentiment
score, keyword count and word cloud built on it is wrong.

This Actor removes quote blocks from the markup before reading the text:

| field | contains |
|---|---|
| `content` | only what this author wrote |
| `quote_text` | the material they quoted |
| `quoted_authors` | who they were quoting |

Nested quotes are handled (every author is captured, no text is double-counted), and
the "Click to expand..." link never appears in either field.

***

### Input

#### Mode

| value | behaviour |
|---|---|
| `forum` | Board threads — one row per thread |
| `thread` | Full thread — opening post plus every reply |

#### Board threads mode

**`forumIds`** — board IDs, the `{id}` in `sammyboy.com/forums/{slug}.{id}/`.
For example `17` is *A Singaporean's guide to Living In JB*.

Must be a board that lists threads, not a category that lists sub-forums. A category
returns no rows and the run tells you so.

#### Full thread mode

**`threadIds`** — the `{id}` in `sammyboy.com/threads/{slug}.{id}/`, e.g. `111952`.

**`threadUrls`** — full thread URLs. The ID is extracted for you.

**`maxPagesPerThread`** — how many pages of replies to walk, counting back from the
newest. `0` means unlimited — see *Performance* below before setting that on a
long thread.

#### Both modes

**`keywords`** — keeps only threads whose title contains at least one keyword
(case-insensitive). Leave empty for no filtering.

**`excludeKeywords`** — drops any thread whose title contains one of these, even if
it also matches `keywords`. Exclusions always win. Useful for stripping SEO spam and
off-topic threads out of a board sweep.

**`matchWholeWords`** — off (the default), a keyword matches anywhere in the title,
so `JB` also matches `JBCC Quayside`. On, it must appear as its own word. Applies to
both keyword fields.

These three apply to **board threads mode only**. In full thread mode you have already
named the threads, so every reply is returned regardless of what you put here.

**`dateFrom`** — `3d`, `12h`, `2w`, or `MM/DD/YY`.

- Board threads mode: filters by the newer of creation and last-reply time, and
  **stops paging early** once it passes the cutoff. Pinned threads are exempt —
  they sit at the top of every board regardless of age, so without the exemption
  one 2012 sticky would end the crawl on row one.
- Full thread mode: filters replies by post time. Because threads are walked
  newest-first, this stops as soon as a page falls past the cutoff — so pulling the
  last two years of a thread started in 2012 costs a few pages, not all of them.
  The opening post is always returned.

**`maxItems`** — board mode: max threads per board. Thread mode: max *threads*, not
replies. Use `maxPagesPerThread` to bound replies.

**`bulkInput`** — one item per line, no quotes or commas. Treated as board IDs,
thread IDs or thread URLs depending on the mode. Paste a column straight out of
Google Sheets.

***

### Performance and cost

Sammyboy sits behind Cloudflare, so every page is fetched through Apify's Unblocker
proxy. That sets the pace of every run:

- **About 8 seconds of wall clock per page.**
- A 100-page thread therefore takes **roughly 13 minutes**.
- The proxy admits only two to three requests at a time, so this can't be sped up
  by asking for more. Concurrency is fixed at the fastest setting and isn't
  exposed as an input.

`dateFrom` stops pagination as soon as it passes the cutoff, so a narrow date
window costs a fraction of a full sweep — it's the cheapest way to keep runs
short. A `keywords` filter on its own does not stop paging, so pair the two when
you're sweeping a large board.

***

### Limitations

**Reply and view counts are abbreviated.** Sammyboy renders them as `"2K"` and
`"784K"` and does not expose the exact figure anywhere in the page, so they are
returned as the site shows them. Treat them as approximate.

**Boards are not categories.** A category page lists sub-forums rather than threads
and returns nothing. Use a sub-forum ID.

**Thread mode returns replies newest-first**, which is deliberate — it is what makes
`dateFrom` cheap on long threads. Sort by `floor` ascending if you need chronological
order.

**Forum content includes spam.** Like most open forums, some boards carry SEO
link-drops and off-topic posts. Filter downstream.

***

### Notes

Only publicly visible pages are read — nothing behind a login, and no personal data
beyond the public usernames attached to public posts. You are responsible for how
you use the output, including any applicable terms and data-protection rules.

Built as a sibling to the HardwareZone and RenoTalk scrapers. All three emit the same
field names and the same two output shapes, so datasets from different Singapore
forums can be merged without remapping columns.

# Actor input Schema

## `mode` (type: `string`):

Board threads returns one row per thread and never opens it. Full thread returns a thread's opening post plus every reply.

## `forumIds` (type: `array`):

The number in a board URL: sammyboy.com/forums/{name}.{id}/ — for example 17 in /forums/a-singaporeans-guide-to-living-in-jb.17/. Use a board that lists threads, not a category that lists other boards.

## `keywords` (type: `array`):

Keeps only threads whose title contains one of these words. Case-insensitive. Leave empty to keep every thread.

## `excludeKeywords` (type: `array`):

Drops any thread whose title contains one of these words, even if it also matches the filter above. Useful for stripping out spam and off-topic threads.

## `matchWholeWords` (type: `boolean`):

Off, a keyword matches anywhere in the title, so "JB" also matches "JBCC". On, it must appear as its own word. Applies to both fields above.

## `threadIds` (type: `array`):

The number in a thread URL: sammyboy.com/threads/{name}.{id}/ — for example 111952.

## `threadUrls` (type: `array`):

Full thread URLs, e.g. https://www.sammyboy.com/threads/renovation-to-your-homes.111952/. The ID is read from the URL for you.

## `maxPagesPerThread` (type: `integer`):

How many pages of replies to read, counting back from the newest. Each page takes about 8 seconds, so a 100-page thread runs for roughly 13 minutes. Set 0 to read the whole thread.

## `bulkInput` (type: `string`):

One item per line, no quotes or commas. Read as board IDs, thread IDs, or thread URLs depending on the mode. You can paste a column straight from a spreadsheet.

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

Board threads mode: how many threads to return per board. Full thread mode: how many threads to open — this does not limit replies, use Max pages per thread for that.

## `dateFrom` (type: `string`):

Accepts 3d, 12h, 2w, or MM/DD/YY. Leave empty for no date filter. Board threads mode: filters on the later of a thread's start date and its last reply, and stops reading further pages once it passes the cutoff. Pinned threads are always returned, since they stay at the top of a board however old they are. Full thread mode: filters replies by date and stops early, because replies are read newest first. A thread's opening post is always returned.

## Actor input object example

```json
{
  "mode": "forum",
  "forumIds": [
    "17"
  ],
  "keywords": [],
  "excludeKeywords": [],
  "matchWholeWords": false,
  "threadIds": [],
  "threadUrls": [],
  "maxPagesPerThread": 0,
  "bulkInput": "",
  "maxItems": 50,
  "dateFrom": ""
}
```

# 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 = {
    "forumIds": [
        "17"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("lexac94/sammyboy-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 = { "forumIds": ["17"] }

# Run the Actor and wait for it to finish
run = client.actor("lexac94/sammyboy-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 '{
  "forumIds": [
    "17"
  ]
}' |
apify call lexac94/sammyboy-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lexac94/sammyboy-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/Tf3pZPsZjkMHxAYvL/builds/D5zD6wzXbxq4dhXmP/openapi.json
