I published a data tool to a marketplace. The code took a day. I pulled the logic into pure functions pinned by tests, hit the real API for two districts, and confirmed all 32 response tags were mapped. Locally it produced 297 rows and exit code 0.
Then it took four rejections to press "Publish".
A question up front. Does your deploy checklist contain only the items the platform rejects out loud? Mine did. The fifth item was not on it.
Wall 1 — environment variables are snapshotted into the build
Making each customer issue their own API key kills self-serve conversion. So I put my key into the actor's environment variables as a secret and ran with no key in the input.
[apify] INFO Initializing Actor ...
[apify] INFO Exiting Actor ({"exit_code": 91})It could not read the key. The variable was set — but the value does not attach to a build that already exists. A rebuild fixed it immediately.
[apify] INFO 11110 202607 page 1 → 36 rows (total 36)
[apify] INFO Exiting Actor ({"exit_code": 0})I had already hit this shape on another platform: an environment variable added after the deploy silently failed to load while the endpoint kept returning success. I wrote down the rule then — "if you set env, redeploy and read it back" — and still stepped on it here. I had memorized the rule attached to a platform name.
Wall 2 — the output schema needs a type on every property
Pressing publish produced this:
Add "Output" schema(s) to the source code and rebuild the Actor before publishing.
It is mandatory because agents need to know the shape of your results. I copied the documented example, and the validator rejected it.
Error: Output schema is not valid:
- must have required property 'type' at /properties/transactionsThe minimal example in the docs has no type. Adding "type": "string" to each property passes. I defined three: JSON download, CSV download, console link.
Wall 3 — publishing checks your profile and your terms first
I tried to flip the public switch through the API. Two 403s in a row.
403 username-required
"Actor owner needs to have a public profile in order to publish the Actor."
403 store-terms-not-accepted
"The Actor owner must accept the Apify Store terms and conditions..."Something else was hiding here. Opening the settings page to make the profile public, I found the template's sample bio still sitting in the field — a joke about writing scrapers on a 1975 computer. Publish as-is and that becomes my bio in the store. The README field was still the commented-out example.
Wall 4 — the example input was the template default
Publishing worked. And the confirmation dialog slipped this past me:
Your Actor will be auto-tested with its default input every day. If it doesn't succeed with a non-empty default dataset in 5 minutes 3 days in a row, we'll mark it as under maintenance.
I queried the actor object through the API.
"exampleRunInput": { "body": "{ \"helloWorld\": 123 }" }The project template's default, untouched. With that input my code fails instantly. Nobody tells you about today's failure. The badge lands three days later. I overwrote it with a real input (a district code and a query month).
That is four. All four were announced by the platform, and together they cost about an hour. Walls that shout are the cheap kind.
The fifth came out of auditing the gate
After publishing I went back to my habit of asking "what is the one number that matters in this experiment". While doing that, one combination caught my eye:
- The tool is free and public.
- And to remove friction, I shipped my own API key inside it.
The source API allows 10,000 calls a day on a dev account. But the input is a list of regions times a date range. If a stranger enters 250 districts and 24 months, a single run is 6,000 calls. Two runs end the day's quota.
What happens next is the real problem. When the quota dries up the actor fails. Three days in a row and you get the badge from wall 4. At which point the number I planned to read in 30 days — "do strangers find and run this without any channel of mine" — becomes unmeasurable. No error log warns you in advance, because nothing happens until strangers show up.
I have watched a metric get invalidated before: the visitors that passed my gate were my own screenshots. That time the denominator was contaminated; this time the instrument itself was about to switch off.
Would you wait until the quota dries and then react, or bound the input now?
The fix is one pure function
def request_budget(n_regions: int, n_months: int, limit: int) -> int:
n = n_regions * n_months
if n > limit:
raise ValueError(
"too many combinations: %d regions x %d months = %d (limit %d). "
"Reduce regions or the period and run again."
% (n_regions, n_months, n, limit))
return nThe limit is 240 (10 regions × 24 months), pinned by a test.
assert request_budget(10, 24, 240) == 240
try:
request_budget(25, 12, 240)
raise AssertionError("over the limit but it passed")
except ValueError as e:
assert "limit 240" in str(e)That test runs during the container build, so if the guard breaks, the build fails rather than the deploy succeeding. Ever since two weeks of green workflows over a site that had gone stale, I hang checks on the deploy path rather than on runtime.
Three self-checks
- Did you read back the environment variable or secret you just set, from a fresh build where the value is actually loaded?
- If the platform runs something on a schedule for you (health check, default-input test, cron), is that input a value that actually passes — not the template default?
- Does a tool you published for free carry your account's credentials or quota? If so, have you bounded in code the maximum a single run can burn?
The honest part
One thing is still unverified: I could not find documentation stating that the person running a free public tool pays the platform compute. I believe that is how it works, but I am not asserting it without a source — it is on the list to confirm from the billing screen once a stranger's run appears.
And this is not a success story. In 30 days there may be zero users. What changed is that the zero will not be "the tool was dead because the quota dried up" kind of zero. Today's result is that one quiet defect, the kind that can void an experiment, got closed before strangers arrived.
Is your account's quota sitting inside something you published for free?