DEV Community

czcz0009
czcz0009

Posted on

3 Traps I Hit Building Apify Pay-Per-Event Actors as a Solo Node.js Dev

Why Apify Store?

As a Node.js/TypeScript engineer with a day job, I've been looking for a side project that fits into 15-minute-to-1-hour fragments of time, doesn't require sales calls or synchronous client work, and doesn't need me to build my own distribution channel.

Apify Store checked those boxes: it's a marketplace, so discovery (search, categories) is handled by the platform. You build, you publish, and — in theory — people find you through search.

This post covers three concrete traps I ran into while building a handful of pay-per-event (PPE) Actors, in case they save someone else a debugging session.

Trap 1: The hidden double-charge from apify-default-dataset-item

Apify's PPE model charges whenever you call Actor.charge('your-event-name'). I assumed that was the only thing being billed. It isn't.

There's a built-in synthetic event called apify-default-dataset-item that auto-charges $0.00001 every time you call Actor.pushData() into the default dataset — regardless of whatever custom charge events you've already fired for that same record.

If your main.js charges product-listed for each item and then also pushes that item to the default dataset, you're billing the same unit of value twice without realizing it.

Fix: open your Actor's Pay-per-event settings in Apify Console and check chargedEventCounts after a real run. If apify-default-dataset-item shows up alongside your own events, delete it from the Console-side event list.

Trap 2: apify-actor-start billing scales with memory allocation

apify-actor-start is Apify's synthetic "run start" event. It covers the first 5 seconds of compute for free and Apify explicitly recommends keeping its price at the default ($0.00005) to stay competitive.

What's easy to miss: the number of times this event is charged scales with your memory allocation. Every full 1GB over the base counts as an extra charge. I had an Actor running with memoryMbytes: 4096 while actual peak usage was around 100MB — a 40x over-allocation that was quietly multiplying this charge.

Fix: set defaultMemoryMbytes (and minMemoryMbytes/maxMemoryMbytes) based on your actual measured peak, not a safe-sounding round number.

Trap 3: ToS-clean API, IP-allowlist blocked

This one wasn't a pricing bug — it was an infrastructure mismatch. I integrated a public, officially documented, commercial-use-approved API from a Japanese travel platform. Registration was instant, no review process. Great, I thought — this is the clean path.

Then every run on Apify failed with 403 CLIENT_IP_NOT_ALLOWED. The API required registering an allowed IP address, and Apify's serverless runs use dynamic, shared IPs that change per run — there's no fixed IP to register.

Options I considered:

  • Apify's Dedicated Proxy Group — a paid product that gives you a fixed IP range you can register. Reliable, but adds recurring cost.
  • Spoof the request origin to bypass the restriction — technically possible, but this crosses from "the ToS is ambiguous" into "actively defeating an access control the provider put in place." I ruled this out.
  • Widen the allowlist to 0.0.0.0/0 via the provider's own registration form — this uses the provider's own official input format for exactly this purpose. Not a workaround, just a config choice with a real tradeoff: your API key becomes the only line of defense, so treat it as isSecret: true and keep it out of your repo.

I went with the third option. It's a legitimate setting, but it does mean you're now relying entirely on credential secrecy instead of network-level restriction — worth flagging in your own docs if you build something similar.

How I think about ToS gray zones

A lot of Japanese-market data sources fall somewhere between "explicitly fine" and "explicitly forbidden." When deciding whether to proceed, I look at four things:

  1. Is there a specific liquidated-damages clause? (e.g., "X yen per unauthorized item") — if so, treat it as a hard stop.
  2. How aggressive is the bot defense? (Cloudflare Bot Management vs. nothing) — aggressive defense usually means the operator cares a lot, which correlates with risk.
  3. Is there an explicit anti-AI stance? (e.g., robots.txt blocking GPTBot/Google-Extended) — a signal the operator has thought about this exact use case and said no.
  4. Is there existing, tolerated commercial scraping activity in the same space? — not a legal green light, but a useful signal for how the operator has historically responded.

None of this is legal advice — just the framework I use to decide what's worth the risk of an IP block versus what isn't.

What I ended up building

Applying all of the above, I published a handful of PPE Actors around Japanese-market data — VRChat-related asset listings, real-estate yield estimation, PR-release sales-signal detection, hotel recommendations, and used-car valuation. All of them are early and still building up usage, but the architecture and the lessons above are the same across all of them.

If you're curious about the details or want to see the PPE pricing/output-schema setup in practice:

Happy to answer questions about PPE pricing design, dataset schemas, or any of the ToS-judgment stuff above in the comments.

Top comments (0)