I published eight Actors on Apify Store in five days. Careful titles, real READMEs written from real runs, dataset schemas, published task examples, honest pay-per-event pricing. Then I searched the Store for the exact thing I had built and could not find myself.
Not "found low". Not found at all in the first fifty results.
So I stopped guessing and measured it. The answer is one number, it is not the number I had been optimising, and the whole week of title tuning was wasted effort.
The thing that looked like proof
Earlier in the week I renamed an Actor and watched it vanish from search, then reappear a few hours later at positions 12, 7 and 26 for three related terms. Another Actor appeared at position 10 for its main keyword three hours after I published it.
Rename, then rank. Publish, then rank. Obvious: Store search matches on title.
That was wrong, and the shape of the error is worth naming. I had two events and an interpretation, and I never ran the control.
The control
Search the Store for greenhouse jobs. Look at who is above me.
# users30 actor
1 114 fantastic-jobs/greenhouse-jobs-api
...
9 0 vnx0/greenhouse-jobs-scraper
13 0 skootle/greenhouse-jobs
15 0 charliemorrisondev/greenhouse-jobs-scraper
Three Actors at positions 9, 13 and 15 with zero users in the last thirty days. My dododata/greenhouse-jobs-scraper has the same name, a longer README, a dataset schema, published task examples and a working price, and it is at 74.
Identical names. Sixty places apart. Title match cannot explain that.
The difference is in a field I had not been looking at, because the Store UI shows you the monthly number:
| Actor | position | users, 30 days | users, lifetime | runs, lifetime |
|---|---|---|---|---|
| vnx0/greenhouse-jobs-scraper | 9 | 0 | 12 | 236 |
| skootle/greenhouse-jobs | 13 | 0 | 5 | 196 |
| charliemorrisondev/greenhouse-jobs-scraper | 15 | 0 | 4 | 193 |
| dododata/greenhouse-jobs-scraper | 74 | 1 | 2 | 11 |
The correlation
One request per term, then Spearman's rho between result position and each stat over the first thirty to forty results.
import json, urllib.request, urllib.parse
def spearman(x, y):
def rank(v):
order = sorted(range(len(v)), key=lambda i: v[i])
r = [0] * len(v)
for pos, i in enumerate(order):
r[i] = pos
return r
rx, ry = rank(x), rank(y)
n = len(x)
d2 = sum((rx[i] - ry[i]) ** 2 for i in range(n))
return 1 - 6 * d2 / (n * (n * n - 1))
url = "https://api.apify.com/v2/store?search=" + urllib.parse.quote("greenhouse jobs") + "&limit=40"
items = json.load(urllib.request.urlopen(url))["data"]["items"]
inv_position = [-i for i, _ in enumerate(items, 1)]
lifetime = [(a.get("stats") or {}).get("totalUsers", 0) for a in items]
monthly = [(a.get("stats") or {}).get("totalUsers30Days", 0) for a in items]
print(spearman(inv_position, lifetime), spearman(inv_position, monthly))
Three terms:
| Search term | lifetime users | lifetime runs | 30-day users |
|---|---|---|---|
| greenhouse jobs | +0.934 | +0.645 | +0.111 |
| app store reviews | +0.930 | +0.864 | +0.692 |
| shopify products | +0.871 | +0.574 | +0.387 |
Rank is cumulative distinct users, all time. The thirty-day number that the UI puts in front of you barely registers.
This also explains the two events that fooled me. A rename does not change your rank; it briefly removes you from the index and then puts you back where you always were. And an Actor that "appeared at position 10" appeared there for a keyword where the whole field was near zero, so two lifetime users was enough.
What that means if you are new
The metric never decays. That cuts both ways.
It is a ratchet, so every real user you ever get is permanent rank equity. It is also a wall, because the people above you banked theirs months ago and keep them for free. Paging the search API to the end, here is where five days of work actually put me:
| Term | my rank | lifetime runs of the lowest Actor in the top 20 |
|---|---|---|
| workday jobs | 49 | 57 |
| greenhouse jobs | 74 | 57 |
| shopify products | 117 | 102 |
| app store reviews | 174 | 153 |
You cannot fake it either, and not only because faking things on a platform that counts them is a bad way to run a business. totalUsers counts distinct users, so running your own Actor a thousand times moves nothing.
The practical conclusion is uncomfortable and clarifying at the same time. On-platform discovery for a new account is a closed loop: search is how people find you, search is ordered by usage, and usage comes from search. Your first users cannot come from the Store. They have to come from somewhere else, and then the Store starts working for you.
The one surface that is not gated on usage
There is exactly one: sortBy=newest. It orders by publication date and nothing else.
https://api.apify.com/v2/store?sortBy=newest&limit=100
My newest Actor was at position 17 four hours after publishing, while the seven older ones sat at 728 to 745. Measuring the age of the Actor at each depth gives the decay:
| Position | Age of the Actor there |
|---|---|
| 1 | 1 hour |
| 101 | 17 hours |
| 301 | 33 hours |
| 701 | 61 hours |
About six new Actors an hour, platform-wide. So publishing buys you roughly three hours on the first page of newest and about a day in the top hundred.
Three hours is not much. But it is the only window a zero-history account gets, and it has a scheduling consequence I had completely backwards: never publish two Actors on the same day. I had three finishing staging together and was going to ship them in one batch. That would have spent three windows to buy one. They now go out on three separate days, each one paired with whatever off-platform writing lands the same afternoon, because those hours are the only time the platform itself is sending anyone.
The bug in my own measurement
Worth repeating because it nearly became a published claim.
My first pass said my Actors were missing from the Jobs category page entirely, which would have been a much more dramatic post. The loop looked like this:
off = 0
while True:
items = get(f"...&category=JOBS&limit=100&offset={off}")
if not items:
break # <-- wrong
off += 100
The Store listing endpoint intermittently returns an empty page in the middle of a result set. Treating that as the end of the list stopped the scan at 994 records, my Actors were not in those 994, and the conclusion wrote itself. Retrying an empty page shows the category holds 6,066 Actors and mine are all in it, at positions 1,192 to 1,213. Buried by the same variable as everything else, not excluded.
If you paginate this API, retry an empty page before you believe it.
What I would tell myself on day one
Spend day one on distribution and day two on the Actor. Not the other way round, which is what I did.
The build quality still matters, just not for the reason you think. It does not rank you. It is what makes the handful of people who arrive from somewhere else come back, and a returning user is the only input to the one number that does rank you.
The Actors are at apify.com/dododata if you want to see what eight Actors and two lifetime users look like. Public sources only, no personal data, pay per event. Ask me anything about the measurement and I will run it again with your term.
Top comments (0)