Apify Content Program Theme — Actors that plug into your stack · your Actor as a tool for AI agents

Engineering write-up · from a live deployment

Apify Actors wired into a live game’s data loop.

Robotgames runs a daily social sweep through six public Store Actors, resolves runs and Dataset writes across two Apify accounts, mirrors its game DB into named Datasets on demand, and publishes its own pay-per-result Actor.

Use cases
14
Live
11
On Apify
6
Accounts
2
Per 1k rows
$1

§01 · The problem

Three reasons to put Actors and Datasets in the loop

The integration begins with a collection problem, continues with a controlled export problem, and ends with a product problem. Apify provides a distinct boundary for each one.

Collect social sentiment without maintaining six crawlers. Instagram, TikTok, and YouTube each need a discovery stage and a comments stage. Six public Store Actors absorb anti-blocking work and markup churn; Robotgames keeps the smaller job of normalizing their Dataset items, classifying comments, and storing the result.

Share analytics without opening the production database. An on-demand mirror turns selected SQLite tables into normalized, named Apify Datasets. Credential-like columns are redacted by default, and --analytics-only excludes the project’s enumerated PII tables before upload.

Turn an internal workflow into a callable product. The published Market Sentiment Marker accepts a typed JSON input, writes a marker row before its breakdown and optional evidence rows, and requests one result event per Dataset row at the configured pay-per-result price.

The full deployment register appears below with Apify as the only provider named; the others are anonymized rather than re-attributed.

§02 · Architecture

Three checked-in Apify paths

The daily path consumes public Actor output, the mirror publishes selected database tables into named Datasets, and the Store Actor exposes a related sentiment workflow as a marker-first JSON contract.

Fig. 1 — the Dataset boundary works in both directions: Actor output enters the game, while selected game tables can be published back into named Datasets.

The sweep, unrolled

06:15 UTC nominal · randomized delay up to 900 s six public Store Actors · discovery then comments per platform IG discover IG comments TikTok discover TikTok comments YT discover YT comments classify SQLite store each Actor: 15 min local deadline + requested platform timeout · default Dataset paged in 1,000-row batches

§03 · The code

Three real Apify paths, condensed

These fragments keep the deployed control flow and names. Repetitive guards, inputs, logging, and presentation fields are trimmed; the integration semantics are not replaced with sample SDK code.

1 · Daily sweep: start → poll → Dataset items → SQLite

The dependency-free client starts a public Actor, polls its run, then pages through the default Dataset. The orchestrator performs discovery and comments as separate stages for each of three platforms, classifies comments, and upserts social_posts, social_comments, and social_runs. The timer fires once daily at 06:15 UTC, with up to 900 seconds of randomized delay.

server-node/scripts/social/apify.js + scrape.jscommonjs · actor rest api
async function runActor(actorId, input, opts = {}) {
  const cfg = { ...DEFAULTS, ...opts };
  const started = await api('POST',
    `/v2/acts/${actorId}/runs?timeout=${cfg.runTimeoutSecs}`,
    { body: input });
  const { id: runId, defaultDatasetId: datasetId } = started.data || {};

  let status = started.data?.status || 'READY';
  while (status === 'READY' || status === 'RUNNING') {
    await sleep(cfg.pollMs);
    status = (await api('GET', `/v2/actor-runs/${runId}`)).data.status;
  }

  const items = [];
  for (let offset = 0; ; offset += 1000) {
    const page = await api('GET',
      `/v2/datasets/${datasetId}/items?clean=true&format=json&limit=1000&offset=${offset}`);
    items.push(...page);
    if (page.length < 1000) break;
  }
  return { items, runId, status, datasetId };
}

const ACTOR_STAGES = {
  instagram: ['apify~instagram-hashtag-scraper', 'apify~instagram-comment-scraper'],
  tiktok: ['clockworks~tiktok-scraper', 'clockworks~tiktok-comments-scraper'],
  youtube: ['streamers~youtube-scraper', 'streamers~youtube-comments-scraper'],
};

// discovery items → normalized post URLs → comment Actor items
const byPlatform = await runConfiguredScrapers(ACTOR_STAGES);
await classifyComments(allComments);
store(db, allPosts, allComments, Date.now()); // social_* SQLite tables

2 · Two accounts: environment first, then local fallback

Account #2 (s4ndid) is active and preferred. Account #1 (angseesiang) is out of credit but remains the Store-listing owner. The resolver is CommonJS, checks process.env before ~/.claude.json, and returns metadata without printing a token. Because IDs differ per account, its Dataset and Actor helpers list the active account and match exact names.

server-node/scripts/apify-token.jscommonjs · two accounts
'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');

let claudeCfg;
function fromClaudeJson(key) {
  if (claudeCfg === undefined) {
    try {
      claudeCfg = JSON.parse(fs.readFileSync(
        path.join(os.homedir(), '.claude.json'), 'utf8'));
    } catch (_) { claudeCfg = null; }
  }
  return (claudeCfg && claudeCfg[key]) || '';
}

function resolveApifyToken() {
  for (const [envVar, account] of [
    ['APIFY2_API_TOKEN', 2], ['APIFY_API_TOKEN', 1],
  ]) {
    const token = process.env[envVar] || fromClaudeJson(envVar);
    if (token) return { token, account, envVar };
  }
  return { token: '', account: null, envVar: null };
}

// datasetIdByName() and actorIdByName() resolve exact names
// on the active account because resource IDs are account-specific.
module.exports = { resolveApifyToken, datasetIdByName, actorIdByName };

3 · Published Actor: marker first, then billable rows

The Store Actor assembles a bounded output array before writing anything. The marker is pushed first, breakdown rows follow, and optional raw evidence fills the remaining allowance. After the Dataset write, the Actor requests one result charge event per emitted row.

apify-actor/all-in-one-market-sentiment-marker/main.jsesm · pay per result
const maxItems = Number.isInteger(input.maxItems) && input.maxItems > 0
  ? input.maxItems : 0;
const includeRawItems = input.includeRawItems !== false;

const out = [];
const pushRow = (row) => {
  if (!maxItems || out.length < maxItems) out.push(row);
};

// Always row 1, even when maxItems caps the Dataset.
const marker = {
  _kind: 'market_sentiment', brand,
  index: overall.index, label: overall.label,
  positive: overall.positive, negative: overall.negative,
  neutral: overall.neutral, totalVoices: overall.total,
  platforms: perPlatform,
  competitors: competitorMarks.map((c) => ({
    competitor: c.competitor, index: c.index,
    label: c.label, total: c.total,
  })),
  scannedAt: new Date().toISOString(),
};
pushRow(marker);

for (const [platform, tally] of Object.entries(perPlatform))
  pushRow({ _kind: 'platform_sentiment', brand, platform, ...tally });
for (const competitor of competitorMarks)
  pushRow({ _kind: 'competitor_sentiment', brand, ...competitor });

if (includeRawItems) {
  for (const post of posts) pushRow({ _kind: 'post', brand, ...post });
  for (const comment of comments) pushRow({ _kind: 'comment', brand, ...comment });
  for (const item of news) pushRow({ _kind: 'news', brand, ...item });
}

await Actor.pushData(out);
try {
  if (out.length) await Actor.charge({ eventName: 'result', count: out.length });
} catch (err) { log.warning(`Charging skipped: ${err.message}`); }

§04 · The published Actor

ALL-IN-ONE Market Sentiment Marker

Give it a brand and select from four supported channels — Instagram, TikTok, YouTube, and Google News. It classifies social comments and news headlines, then returns a Market Sentiment Index from −100 to +100, breakdown rows, and optional raw evidence rows.

Only brand is required; News is the default channel. Social channels call the same six public Store Actors used by the game’s collection pattern. The marker formula is round((positive − negative) / total voices × 100). The input field for rivals is competitors, and the current competitor path benchmarks Google News results rather than all four channels.

The Actor writes a marker row first, then per-platform and per-competitor breakdowns, followed by optional post, comment, and news rows. Its pay-per-result event is $0.001 per dataset row — $1 per 1,000 rows. Social sub-Actors are billed separately by their authors, so includeRawItems: false, maxItems, and the collection limits are real cost controls.

The JSON input and Dataset output make the Actor usable as a tool contract for an AI-agent orchestrator. This repository demonstrates the Actor and its API-shaped contract; it does not claim a deployed MCP integration for this Actor.

Two-account boundary: account #1 (angseesiang) owns the published listing but is out of credit. Scheduled runs and writes prefer active account #2 (s4ndid) through apify-token.js. Account-specific Actors and Datasets are resolved by name because their IDs differ.

§05 · The register

Fourteen use cases with their true status split

The six Apify rows are quoted verbatim from the console register; the other eight keep their verbatim titles and statuses, with provider names and provider-derived identifiers redacted from the descriptions. The “where it runs” column is derived from checked-in paths and timer units.

Account #1 · angseesiang: Account #1 is out of credit but is kept because it OWNS the published Store listing; execution moved to Apify 2 via the token resolver.
Account #2 · s4ndid: active execution account for scheduled runs, Dataset writes, replicated Actors, and side-by-side spend visibility.
live · 11 gated · 1 dormant · 2
Robotgames business-case register · 14 rows · 11 live / 1 gated / 2 dormant
AppUse caseWhere it runsStatusBusiness value · disclosed redactions
Apify #1 Social sentiment monitoring (IG / TikTok / YouTube) timer · 06:15 UTC daily live Daily scrape (robotgames-social.timer, 06:15 UTC) of posts and comments about the game; each comment classified positive / negative / neutral into social_* tables — a community-health trendline for marketing. Pipeline conceived on this account, now billed to Apify 2.
Apify #1 Monetized Store Actor “ALL-IN-ONE Market Sentiment Marker” Apify Store live Public pay-per-result Actor ($1 per 1k rows): type a brand, get one Market Sentiment Index (−100…+100) across Instagram / TikTok / YouTube / News plus competitor benchmarking — the internal sentiment engine productized as revenue. The Store listing lives on this account.
Apify #1 Game-DB mirror & data export manual / on demand live push-to-apify.js mirrors each game table into named Apify Datasets (browsable, CSV/JSON/XLSX export) with credential redaction and analytics-only PII exclusion — a shareable analytics mirror without opening the prod DB.
Apify #1 Account & spend observability admin console · account tab live This tab reads plan, monthly usage vs cap, actors and recent runs — finance visibility on the account that still owns the public listing.
Apify 2 Active execution backend for all Apify work server scripts · active token resolver live apify-token.js prefers APIFY2_API_TOKEN, so the daily social scrape, the DB→dataset mirror and actor builds all run and bill here — business continuity after account #1 ran out of credit, with zero pipeline changes.
Apify 2 Fleet replication & side-by-side cost observability manual / on demand · admin console live replicate-account.js cloned every Actor (source + env + build) from #1 onto this account; this tab tracks its own plan, usage cycle, actors and runs next to #1 so spend is visible per account.
Proxy-network unlocker Chat phishing & dangerous-link guardrail server · chat hot path live Every link a player posts in chat is dereferenced through an enterprise proxy network (never from the game box or players), shorteners and cloaked redirects are unmasked, and the landing page is scored for phishing / credential-harvesting / brand-impersonation. Dangerous links are blocked before broadcast — player safety on the chat hot path, fail-open so chat never stalls.
Proxy-network unlocker Trust & safety audit trail + threat intel SQLite · audit trail live Every scan is persisted to a dedicated scans table (sender, URL, verdict, score, reasons, blocked) — a durable ops record proving the guardrail works, powering this tab’s verdict trends and top-flagged-hosts, and doubling as a feed of abusive domains.
Web-agent suite In-game web search for players server API · on demand live Authenticated GET /api/search proxies the search provider server-side, so players get web search inside the game while the paid key never reaches the client.
Web-agent suite Community listening (Reddit / Steam / HN / forums) timer · 06:45 UTC daily live Daily sweep (06:45 UTC) of the text communities Apify misses, written into the same social_* tables — wider sentiment coverage at $0 using the free Search+Fetch tier.
Web-agent suite Competitor tracking timer · 07:15 UTC daily live Daily (07:15 UTC) monitoring of rival mech/MOBA games (War Robots, Mecha BREAK, MechWarrior Online, Heavy Metal Machines): patch notes, pricing and events are hashed and diffed into a triage change-feed. Deep structured diffs via the billed Agent stay gated behind a deep-diff env flag.
Web-agent suite Real-world signals → PvE “arena mood” modifier timer · every 6 h · feature gated gated Genre news scored every 6h into a bounded ±5% PvE-only modifier — a live-ops engagement hook. The data pipeline runs, but matches ignore it unless WORLD_MODIFIERS_ENABLED=1 (hard-clamped, PvE only), so the default effect on live games is zero.
Web-agent suite API-less dashboard sync / asset audit manual only · no enabled targets dormant The billed Agent can log into third-party dashboards that have no API and pull structured snapshots. By design it only runs explicitly registered + enabled targets; with none enabled it spends 0 credits and has no timer.
Scraping API Market-research briefs for strategy & roadmap offline / on demand dormant Used out-of-band (offline research job, not on the game’s runtime path) to scrape the market brief rendered below — latest game / MOBA trends, competitor landscape, enhancement roadmap, with an adversarially fact-checked claims ledger. Refreshed on demand; the key stays server-side.

§06 · Trade-offs & lessons

What the Apify paths actually teach

Pay-per-result makes output shape a cost decision

The Store Actor builds a marker-first output array, writes it once, then requests one result charge event per row. Breakdown and optional evidence rows therefore affect the bill.

Datasets are the Actor integration surface

Six public Actor calls across three platforms converge on default Dataset items. One start/poll/retrieve path feeds normalization and classification without pretending the Actor outputs are identical.

Names survive account changes; IDs do not

The resolver prefers account #2 but retains account #1 for the Store listing. Actor and Dataset IDs are account-specific, so helper functions look up the active account’s resources by exact name.

Snapshots beat fake liveness

This page ships no JavaScript and makes no Apify API calls. The source snapshot is labeled 6 Aug 2026, 01:06 instead of presenting static status as a live poll.

Ground rules the shipped page and integrations obey

No client-side keysThis static page contains no tokens and makes no live Apify API calls.
Mirror modes are explicitDB→Datasets redacts credential-like columns by default; --analytics-only is the PII-table exclusion switch.
Register provenance is explicitAll 14 titles and statuses come from BIZ_CASES; six Apify descriptions are verbatim, while provider phrases and identifiers are redacted from the other eight.
Static means staticThe status and account data are a labeled 6 Aug 2026, 01:06 source snapshot, not a live poll.