My Actor ranked #1 in an agent's search. No one else's agent could see it at all.
Four days ago I published a number I was pleased with. I had measured how often an AI agent finds one of my 23 Actors when it searches the Apify Store through the Apify MCP server. The answer came out at roughly one query in five, and on sitemap checker my Actor came back first.
Both figures were wrong. On the queries I re-tested this morning, an agent that is not mine finds my Actors zero times out of six.
The arithmetic was fine. The problem was the connection I measured through, and it is a problem that any Actor author can have without noticing, because the platform gives you no obvious way to measure it any other way.
The measurement I trusted
My script for that article built one URL:
const url = `https://mcp.apify.com/?token=${encodeURIComponent(token)}`;
Then it called search-actors with a keyword, paged through the results and recorded where my Actor landed. sitemap checker put it at rank 1. PDF tables put it at rank 2. I wrote both numbers down and reasoned from them for a week.
I never asked whose search that was.
The same query, with the token and without
search-actors needs a token, so I could not run it anonymously. The Store also has a plain REST endpoint that serves the same shelf, and that one takes the Authorization header or does without it. So I ran the same query twice:
// store-auth-diff.mjs — is my Actor in the results, or only in *my* results?
const TOKEN = process.env.APIFY_TOKEN;
async function search(query, withAuth) {
const url = `https://api.apify.com/v2/store?search=${encodeURIComponent(query)}&limit=100`;
const res = await fetch(url, { headers: withAuth ? { authorization: `Bearer ${TOKEN}` } : {} });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const { data } = await res.json();
const ids = data.items.map((a) => `${a.username}/${a.name}`);
const at = ids.findIndex((id) => id.startsWith('aiqlabs/'));
return { total: data.total, returned: ids.length, mineAt: at < 0 ? null : at + 1 };
}
for (const q of ['sitemap checker', 'pdf table extractor', 'github repository audit']) {
console.log(q, '\n auth:', await search(q, true), '\n anon:', await search(q, false));
}
Its output on 16 August:
sitemap checker
auth: { total: 339, returned: 78, mineAt: 1 }
anon: { total: 339, returned: 77, mineAt: null }
pdf table extractor
auth: { total: 757, returned: 86, mineAt: 2 }
anon: { total: 757, returned: 84, mineAt: null }
github repository audit
auth: { total: 417, returned: 71, mineAt: 1 }
anon: { total: 417, returned: 69, mineAt: null }
Read the columns in order. total is the same number with and without the token — 339 stays 339. The count of items actually returned is one to three higher when the token is present. The extra items are mine.
I ran it over five queries. With the token my Actors sit at ranks 1, 2, 1, 1 and 5. Without it, none of them is anywhere in the first hundred results.
Same endpoint, same query, one HTTP header apart.
The MCP tool reads the same shelf
That only bears on the earlier article if search-actors ranks the same way the REST endpoint does. It does. Six queries through both, comparing the top five:
| query |
search-actors (token) |
/v2/store (token) |
/v2/store (no token) |
top 5 identical |
|---|---|---|---|---|
sitemap checker |
#1 | #1 | absent | 5/5 |
PDF tables |
#2 | #2 | absent | 5/5 |
GitHub repository |
absent to 30 | #40 | absent | 5/5 |
broken links |
absent to 30 | #29 | absent | 5/5 |
RSS feed |
absent to 30 | #47 | absent | 5/5 |
tech stack |
absent | absent | absent | 5/5 |
The top five agree on every query. Where my Actor appears in both channels, the rank is the same integer. So the first place I published — first place on sitemap checker, for an AI agent — was first place for an agent carrying my token. There is one such agent and I built it.
One query parameter says why
At this point I assumed the token was doing something to the ranking. It is not. The Store endpoint documents a parameter that tells you exactly what is happening, and you can use it without any credentials at all.
From the GET /v2/store reference, on includeUnrunnableActors, describing what the default excludes:
Actors from developers who haven't passed KYC, or full-permission Actors without a large user base
So I ran every query anonymously, twice — once plain, once with that flag on:
// unrunnable-probe.mjs — no credentials anywhere in this file
const q = 'github repository audit';
const base = `https://api.apify.com/v2/store?search=${encodeURIComponent(q)}&limit=100`;
for (const url of [base, `${base}&includeUnrunnableActors=true`]) {
const { data } = await (await fetch(url)).json();
const ids = data.items.map((a) => `${a.username}/${a.name}`);
const at = ids.findIndex((id) => id.startsWith('aiqlabs/'));
console.log(url.endsWith('true') ? 'unrunnable ON ' : 'default ',
'total:', data.total, 'mine at:', at < 0 ? '—' : at + 1);
}
default total: 417 mine at: —
unrunnable ON total: 502 mine at: 1
Across five queries, unauthenticated both times:
| query | default | includeUnrunnableActors=true |
|---|---|---|
sitemap checker |
absent, total 339 | #3, total 352 |
pdf table extractor |
absent, total 757 | #2, total 877 |
github repository audit |
absent, total 417 | #1, total 502 |
google play audit |
absent, total 5,102 | #1, total 5,386 |
http status checker |
absent, total 3,189 | #6, total 3,379 |
Both calls are unauthenticated. The only difference is one documented query parameter.
The totals move. Between 13 and 284 more Actors enter each result set, and mine come back near the top of them. My Actors were never ranked badly. They were filtered out before ranking, for everyone except me.
Which of the two exclusions is mine
The documented filter covers two groups, and it is worth knowing which one you are in, because only one of them is fixed by paperwork. The permission level of any public Actor is readable without a token:
const r = await fetch('https://api.apify.com/v2/acts/aiqlabs~sitemap-checker');
const { data } = await r.json();
console.log(data.actorPermissionLevel, data.isPublic); // LIMITED_PERMISSIONS true
All five Actors I checked come back LIMITED_PERMISSIONS, so the full-permission half of the filter does not apply to them. That leaves the other half, and Apify Console states it plainly:
Insights → Payouts, 16 August 2026. Billing details are registered on the same screen; the identity check is not.
The document I hold keeps being rejected by the verification vendor, which is an open thread with Apify support and not this article's subject.
What matters here is the shape of it. An incomplete account setting removed my Actors from every search except my own, and eleven days of work on titles, categories and output schemas went into Actors that were not in the result set while I was tuning them. Every one of those changes was aimed at Actor quality score, which Apify documents as the ranking input for both Store search and search-actors. Ranking inputs do nothing for a row that never reaches the ranking stage.
The Actor works. That was never the problem.
It is worth being clear about what is and is not broken here, because "invisible in search" and "unusable as a tool" are different failures and only one of them applies.
Name the Actor in the connection string and an agent picks it up as a tool immediately. Mine is pinned into my editor's MCP config, so I asked the agent to audit two well-known repositories. It called the Actor itself, waited for the run, and read the dataset back:
run m3PNCMD8HAAIGFpAq SUCCEEDED in 2.402s 0.0027 compute units 2 items
| requested | resolved to | archived | moved | last push | risk | issues |
|---|---|---|---|---|---|---|
facebook/create-react-app |
react/create-react-app |
no | yes | 547 days | medium |
repo_moved, stale_no_push
|
babel/babel-eslint |
babel/babel-eslint |
yes | no | 1,823 days | critical | repo_archived |
Two and a half seconds, a fraction of a cent, and the agent had something it could act on: one dependency whose repository has quietly moved owner while the old URL still answers, and one that is archived outright. That is the whole point of exposing an Actor over MCP — the agent reaches for it mid-task instead of asking me to go and look.
So the tool definition, the input schema and the output fields all do their job. An agent that has this Actor uses it fine. The failure is one step earlier and entirely invisible from here: an agent that does not already have the name will not find it.
Why your own MCP session cannot tell you
The obvious control is to connect to the MCP server without a token and search again. That is not available:
$ curl -s -X POST https://mcp.apify.com/ -H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
{"error":"invalid_token","error_description":"Missing or invalid access token. Pass an Apify API
token in the Authorization: Bearer <token> header. ..."}
HTTP 401, with no token and with an empty ?token= alike. The server speaks JSON-RPC over HTTP, as MCP requires, and every session belongs to somebody.
This is the part I would ask you to sit with, because it is structural and not personal. If you publish an Actor and want to know whether an agent can find it, the natural move is to connect your own client and search. That test is authenticated by construction. It will show you your Actor whether or not anyone else can see it. Your test passes, and it passes for the one reason that cannot generalise.
Nothing in the MCP documentation warned me about this, and I do not think it should have to. Returning an author their own Actors is a reasonable thing for a store to do — it is what makes testing an unpublished or unreviewed Actor possible at all. The gap is not in the behaviour. It is that the only search surface an Actor author naturally reaches for is the one that cannot be run anonymously.
The four-line check to run before you tune anything
No token, no dependencies, no cost:
// visible.mjs — run: node visible.mjs your-username sitemap checker
const [user, ...words] = process.argv.slice(2);
const url = (extra) =>
`https://api.apify.com/v2/store?search=${encodeURIComponent(words.join(' '))}&limit=100${extra}`;
const seen = async (extra) => {
const { data } = await (await fetch(url(extra))).json();
const at = data.items.findIndex((a) => a.username === user);
return `${at < 0 ? 'absent' : `#${at + 1}`} (total ${data.total})`;
};
console.log('as everyone sees it :', await seen(''));
console.log('including filtered :', await seen('&includeUnrunnableActors=true'));
Run against my own account and against a publisher who is not filtered, on the same query:
$ node visible.mjs aiqlabs sitemap checker
as everyone sees it : absent (total 339)
including filtered : #3 (total 352)
$ node visible.mjs automation-lab sitemap checker
as everyone sees it : #1 (total 339)
including filtered : #6 (total 352)
Two different diagnoses from the same two lines. Mine is absent until the filter comes off. The other publisher is first without it, and slips to sixth with it — which is what a healthy result looks like, because turning the filter on adds thirteen competitors to that shelf.
So: if the first line says absent and the second gives you a rank, your Actor is being filtered out of search for every user but you, and no amount of ranking work will change that until the filter lifts. If both lines say absent, you have an ordinary ranking problem. If the first line gives you a rank, you are visible and can trust the numbers you measure.
I would rather have run those four lines on 5 August than on 16 August.
What I got wrong
I published a reachability figure and a rank without asking whose view produced them. The script that produced them had ?token= written into the URL, by me.
It is the second time in two weeks that I have measured my own visibility from inside my own session. The first was a rank I read off the Store web UI in a browser I was logged into. I caught that one, corrected it privately, and then made the same mistake again in a script — which tells me the lesson is not "be careful with browsers" but something duller: if a measurement can only be taken while authenticated, the number it returns is a statement about you, not about your users.
The earlier article's other findings survive. A default MCP connection really does hand an agent eleven tools with a single Actor among them; limit really does default to 5 and cap at 10; the result pages really are ragged, so a short page is not the end of the list; and broadening a keyword really does push my Actor down — that comparison sits inside one channel, so its direction holds even though both ranks are token-local.
What has to be withdrawn is the headline. One query in five is what I could see. For anyone else's agent, on the queries I re-tested, it was none of them.


Top comments (0)