DEV Community

Cover image for Someone Ran My Scraper 1,251 Times and Paid Me Nothing
SIÁN Agency
SIÁN Agency

Posted on

Someone Ran My Scraper 1,251 Times and Paid Me Nothing

Someone ran one of my public actors 1,251 times in four days. I earned roughly enough to cover a sandwich.

If your first instinct is "billing bug," you're going to lose this game. Mine wasn't broken. The runs were real, the results were real, and every single one was engineered to pay me nothing.

The spike you'll mistake for success

Here's what the dashboard showed: a wall of runs. More traffic in four days than the actor saw the entire previous month. Glance at the top-line chart and that's a growth story. You screenshot it. You feel good.

Then you look at who. In four days: 1,251 runs across 761 different accounts. Almost all on the free plan. Every request through the API, none through the UI. Around 30,000 results pulled.

That same actor had 13 real users the entire previous month.

Free traffic is not free to you

This is where the mental model breaks. A free-tier user isn't a marketing cost you eat for goodwill. On a scraper, every run I serve calls a paid data API upstream, one I pay for per request. So 30,000 free results is 30,000 units of my quota, spent, for a rounding error in revenue.

The attacker pays nothing. I pay the upstream. That's the entire play.

The two-fingerprint tell

Funnel: 761 free-tier accounts collapse to 2 session fingerprints, and one operator — 1,251 runs, ~30,000 results, $0 revenue, your paid API quota drained.

761 accounts sounds like 761 people. It wasn't. When I grouped the runs by session fingerprint, all 1,251 collapsed into two. One fingerprint owned 1,050 runs, the other 183.

That's not 761 users. That's one operator running a script that mints throwaway accounts. The account count is noise. The fingerprint count is the truth. The moment you see two, you stop investigating and start defending.

Why your billing looked fine the whole time

The trap: I checked the money first. Charged events matched the platform's own numbers to the cent. Everything reconciled. So for an hour I assumed the system was working as intended — because technically it was. Billing was flawless. The abuse lived one layer up, in who was allowed to trigger a paid run at all.

Reconciling your billing tells you nothing about whether you should have run the job.

The brake I should have shipped first

The fix is small and boring, which is how you know it's right. Gate the expensive work behind a paid-plan check that fails before the first upstream call:

// Reject free-tier runs before we spend a cent of upstream quota.
const isPaying =
  onPlatformPaidFlag === true ||        // a real paying user on the platform
  (!isOnPlatform && testOverride);      // local/dev smoke test only

if (!isPaying) {
  throw new Error("Paid plan required — no upstream call, no charge.");
}
// ...only paying runs reach the expensive part
Enter fullscreen mode Exit fullscreen mode

Fast-failing here means the farm gets an immediate error and burns zero quota. It's reversible: one block, deleted the day the farm gives up. No refactor, no rewrite.

The one gotcha before you copy this

Don't gate blindly. The platform runs its own automated test of your public actor on a non-paying run to keep it visible in the store. Reject that too and your listing quietly goes empty and sinks in the rankings. Whitelist the platform's own test origin, then throw on everyone else.

Do this today

Ship the brake before you need the forensics. I did it in the wrong order so you don't have to.

If you run public actors, go look at your last big traffic spike right now. Group it by fingerprint, not by account. Tell me what you find in the comments. And if you'd rather your scrapers shipped with the brake already wired in, that's the kind of thing we build at SIÁN Agency.

Written by **Nova Chen, Automation Dev Advocate at SIÁN Agency.

Top comments (0)