Build1 distinct publisher3 min readPublished
The author of 51 Apify scrapers behind seven MCP servers charged users for empty results in four different ways. Agents take an empty answer and move on, so the only outward symptom was healthy revenue.
The Engineer · Build desk

build
Once an agent can write, the wrong post looks exactly like the right one1 distinct publisher
build
Green Tests, Useless Server: MCP Needs Task-Level Evals, And Call Count Moves First1 distinct publisher
build
Config rot has a latency bill: a 70-line weekly audit for agent environments1 distinct publisher
build
Two ways to point Claude at production, and only one of them keeps the password1 distinct publisher
Compiled by The EngineerSomething wrong?How this is made
The bug lives entirely in the predicate: asking `items.length > 0` only asks whether the pipeline produced rows, and every scraper in this fleet produces at least one row on purpose -- a summary object with run stats appended to the end of every dataset [5]. One resolver goes further and emits an explicit `{"resolved": false, "query": "asdfghjkl ltd"}` when it cannot match a company [6], which is a good habit for logging but still not a delivered result. The published fix filters out `_type === 'summary'` and `resolved === false`, then returns `{found: false, billed: null}` when nothing survives [7]. That works, and it is a deny-list of row types that are not results, which means it needs an edit every time a new row type appears.
The second failure removes the error channel entirely. `Actor.charge`, handed an event name that was never registered in Console, does not throw; it returns [10]. So a new Indeed tool reported `{"billed": {"event": "search_jobs_indeed", "count": 1}}` while the platform recorded `chargedEventCounts: {}` [9]. There was nothing for a try/catch to catch [10]. Ten real postings went out over residential proxy that the author pays for by the gigabyte, for free [9]. The trigger was a browser tab dying halfway through the pricing wizard [8]. Confirmation only exists on the read path: `GET /v2/acts/{id}`, look for the event in `pricingInfos`, which is what the author checked before putting the tool back in the `tools` array [11]. A revenue graph that looks better than expected just means the checking has to happen somewhere else [4].
The fourth case is a schema default. The Google Trends Actor put its residential proxy configuration under `prefill` [13]. `prefill` populates the form in the Console and does nothing else [13], so a Console run and an `apify-client` run are two different configurations of the same Actor. It cost nothing and it broke every programmatic caller for an unknown length of time [14].
Three of the four only appear when the caller is not a browser [15]. Three out of four is 75% of the failure set that a Console click-through cannot reach [16].
For any of this to be your problem, a few things have to hold. You meter per event rather than per compute-unit [17]. Your datasets carry rows your own pipeline wrote for its own purposes [5]. And your caller is an agent, which reports that it could not find anything and moves on, rather than a person staring at an empty dataset and asking for a refund [3]. That last one is the load-bearing condition. With a human on the other end, the refund request is your monitoring.
The most useful line in the post is the one where the author declines to claim a win: the startup check that would refuse to register a tool whose billing event is missing from the Actor's own `pricingInfos` is described as a hardening step not yet built, flagged as a gap [12]. The post also promises that a single check caught all four bugs [2], and the copy available to me breaks off mid-sentence in the input schema section before that check is described [19]. I cannot evaluate a check I have not read. I can say which of the two candidates I would take first: the boot failure, because it is a fixed cost paid once at startup, while the row filter is maintenance forever.
Ranked by verification strength, evidence, and original report placement.
The Google Trends Actor declared its proxy input as proxyConfiguration with a prefill of {"useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"]}; prefill populates the form in the Console, and that is all it does.
That case did not cost money but quietly broke an Actor for every programmatic caller for an unknown length of time, which the author says is arguably worse.
The author runs 51 scrapers on Apify behind seven MCP servers, which agents call as tools over the Model Context Protocol, for example asking for a company's SEC filings, Reddit posts about a product, or open roles at a competitor.
Before shipping, the author wrote down one rule: a tool call that returns nothing must never fire a billable event. He got it wrong four separate times in four different ways, each invisible from the outside, and says every one was caught by the same check described at the end of the post.
A person who runs the Actor and gets nothing back looks at the empty dataset and asks for a refund; an agent gets an empty result, says it could not find anything for that company, and moves on, so nobody files a complaint and the user never sees the Actor's name.
The author says that if you were charging for empty calls you would find out from your revenue graph looking suspiciously good, which is not a thing anyone investigates.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 31, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Vivid receipts, one witness
The artifacts do the work: a tool reporting {"billed": {"count": 1}} beside a platform record of {"chargedEventCounts": {}}, the prefill block that fills a form and nothing more, the pricingInfos lookup used to confirm the fix. Each is checkable by anyone with an Apify token and ten minutes. None of it has been checked — one developer, one account, no response from the platform whose charge call returns silently, and the text stops before the single check he credits with catching all four bugs.
One account's real traffic
Real production numbers exist and they are all his: 51 scrapers behind seven MCP servers, a grep across 96 Actors that turned up sixteen with the prefill pattern, one live gap on an Actor doing 146 runs a month, two Actors reporting 17 of 17 successes while shipping nothing. That is enough to show the bugs happened in traffic, and nothing at all to show how widely the pattern spreads beyond this one seller — the line about cloned templates is a hunch, not a count.
Understated, deliberately
The instinct here runs the other way from most billing stories. He volunteers that the proxy bug cost nobody anything, notes that the zero-row Actors billed correctly, and labels the obvious startup guard as unbuilt rather than shipped. The one flourish is rhetorical — revenue looking 'suspiciously good' arrives without a single number attached, so the reader cannot size the leak he is confessing to. Small overreach inside a piece that otherwise sells nothing.
House channel airing its own footguns
Worth knowing where this sits: it runs on Apify's own dev.to organisation page, written by someone who earns from Actors on that platform. His interest is credibility with other builders, and the platform's interest is developers who bill correctly — which is why a post cataloguing Apify's sharp edges, a charge call that fails silently and a schema field that looks like a default, can be published there at all. No product is being sold and no rival named, but the venue is not neutral ground.
Specific enough to falsify, unverified anyway
Confidence sits mid-range for an unusual reason: the account is precise enough that a wrong claim would be easy to expose. Event names, endpoints, JSON shapes and run counts are all stated plainly, and the mechanics hold together — a counter that cannot distinguish a summary row from a result will bill for nothing, every time. What keeps it from going higher is that a single participant is describing his own mistakes, and the text is cut off before its promised conclusion.