DEV Community

Cover image for 3 Months Shipping My First Apify Actor: 64 Users, $200/mo, and Everything I Got Wrong
Boon
Boon

Posted on • Edited on

3 Months Shipping My First Apify Actor: 64 Users, $200/mo, and Everything I Got Wrong

TL;DR — 90 days of an Apify Store actor, by the numbers (as of May 2026):

Metric Value
Time since launch 3 months
Lifetime users 64
Monthly active users 13
Revenue (last 30 days) ~$200
Successful runs (30d) 212 / 230 (92%)
Articles published trying to grow 28
Total views across articles 76
Hardest bug Silent success: runs "succeeded" with 0 items
Biggest fix Country-bound residential proxies (60% → 95% success rate)
Biggest pricing mistake $0.30 per-run start fee killing 58% of customers

Honest writeup. Nothing here is a flex.


I shipped my first paid actor on Apify Store in February 2026. The actor is a Vinted scraper — paste a search URL, get JSON listings, export to CSV/Excel. Niche, but real product-market fit.

Three months in, the dashboard tells me 64 people have used it, 13 are active this month, and I'm netting roughly $200/month after Apify's 20% platform cut. I published 28 articles trying to drive growth and the combined view count across all of them is 76 views. Most articles got 0.

If you're thinking about shipping a paid actor on Apify Store — or any indie SaaS where you don't control the distribution platform — this is what 90 days of that experience actually looks like.

Month 1: Shipping the MVP took 3 weeks. Most of it was anti-bot.

Vinted has no public API. The mechanics of scraping it are well-understood by anyone who's tried — Datadome guards every catalog page, and any sequence of plain HTTP requests gets you a 403 in under 60 seconds.

The pattern that ended up working (and that I'd recommend if you're building anything against a Datadome-protected site in 2026):

// 1. Open the catalog page ONCE in a real Playwright browser
const browser = await playwright.chromium.launch({ 
  proxy: { server: residentialProxyUrl } 
});
const ctx = await browser.newContext();
const page = await ctx.newPage();
await page.goto(`https://www.${domain}/catalog?${query}`);
await page.waitForSelector('.catalog-wrapper', { timeout: 20000 });

// Datadome runs its JS challenge against a real Chromium environment
// with a coherent fingerprint. Cookies land in the page context.
const cookies = await ctx.cookies();
const ua = await page.evaluate(() => navigator.userAgent);
await browser.close();

// 2. Reuse those cookies in a fast HTTP loop with got-scraping
const cookieHeader = cookies.map(c => `${c.name}=${c.value}`).join('; ');
for (let p = 1; ; p++) {
  const res = await gotScraping({
    url: `https://www.${domain}/api/v2/catalog/items?${query}&page=${p}&per_page=96`,
    headers: { Cookie: cookieHeader, 'User-Agent': ua, Referer: `https://www.${domain}/` },
    proxyUrl: residentialProxyUrl,
    responseType: 'json',
  });
  if (res.statusCode !== 200 || !res.body.items?.length) break;
  await dataset.pushData(res.body.items.map(transformItem));
}
Enter fullscreen mode Exit fullscreen mode

The browser does the unlock. The HTTP client does the volume. ~10× faster than driving the browser for every page. Throughput goes from ~50 items/min to ~500 items/min.

What I didn't realize at this stage: solving Datadome in dev mode (local laptop, residential IP I owned) is a completely different game from solving it in production for paying customers in different countries. That fact would cost me 76% of my users in months 2-3.

Month 2: Quietly losing 76% of my users

By week 6, I had 51 lifetime users on the actor. Apify Store's dashboard showed a 91% success rate. The monthly active user count was sitting at 30.

By week 10, monthly active was at 12.

Same 91% success rate. Same product. Two-thirds of users gone, quietly.

Apify Store displays success rate, total runs, and MAU on every actor's public page. It does NOT display a "users who came once and never returned" metric. The metric I cared most about didn't exist anywhere in the dashboard.

I dug into the run-level data:

  • 143 SUCCEEDED runs in 30 days
  • 11 ABORTED runs — users hitting Cancel themselves because they saw nothing happening
  • 3 FAILED runs — visible failures with errors

A success at the process level (exitCode 0, run finished cleanly) doesn't mean a success at the product level (customer got data they paid for). The status display only knows about the former.

The root cause: when Datadome served a challenge page (its JS proof-of-work + fingerprint check), my scraper waited 15 seconds for a catalog selector that would never appear, then logged a warning and continued with whatever cookies it had. Those cookies were from the challenge state, not from a real authenticated session. The subsequent API call returned an empty array. The actor exited with exitCode 0. Apify reported SUCCEEDED.

From the user's perspective: open the dashboard, see ✅ Succeeded, click the dataset, see nothing. They don't file a bug. They just don't come back.

// What I had — silently continues even when the challenge is unsolved
try {
  await page.waitForSelector('.catalog-wrapper', { timeout: 15000 });
} catch {
  log.info('Selector timeout — continuing with current cookies'); // ← bad assumption
}
const cookies = await page.context().cookies(); // challenge cookies, not auth
// ...later: API returns 0 items, run "succeeds"
Enter fullscreen mode Exit fullscreen mode

The 4-line fix that should have been there from day 1

async function isDatadomeChallenge(page: any): Promise<boolean> {
  return page.evaluate(() => {
    const html = (document.documentElement.innerHTML || '').toLowerCase();
    return html.includes('captcha-delivery.com') 
        || html.includes('dd_cookie_test')
        || (document.title || '').toLowerCase().includes('access denied');
  });
}

if (await isDatadomeChallenge(page)) {
  if (session) session.retire();
  throw new Error('Datadome challenge detected — retrying with new session.');
}

// Plus a final-state assertion at the end of every run:
if (totalItems === 0) {
  throw new Error(
    'Zero items extracted. The Vinted page returned no results, '
    + 'or anti-bot blocked all attempts. Verify the URL or try again.'
  );
}
Enter fullscreen mode Exit fullscreen mode

The throw at the end converts a silent SUCCEEDED-with-empty-dataset into a loud FAILED with an actionable message. Customers who used to open an empty dataset and churn now see a clear error and either retry, fix their URL, or contact support. None of those outcomes are silent.

ABORTED runs in the next 14 days after deployment: 0.

The other half of the retention drop: country-bound proxies

Apify's residential proxy pool, by default, rotates across all available countries. So a German user's URL request might be served by a US-based residential IP. Vinted geo-routes when the IP doesn't match the domain — sometimes returning empty results, sometimes 403, sometimes a different country's inventory. Datadome flags the mismatched-IP pattern as suspicious and challenges harder.

const TLD_TO_COUNTRY: Record<string, string> = {
  fr: 'FR', de: 'DE', es: 'ES', it: 'IT', nl: 'NL', pl: 'PL',
  pt: 'PT', be: 'BE', at: 'AT', lt: 'LT', cz: 'CZ', sk: 'SK',
  hu: 'HU', ro: 'RO', hr: 'HR', fi: 'FI', dk: 'DK', se: 'SE',
  ee: 'EE', gr: 'GR', ie: 'IE', lu: 'LU', lv: 'LV', si: 'SI',
  'co.uk': 'GB',
};

const country = TLD_TO_COUNTRY[domain.split('.').slice(-2).join('.')];
const proxyConfig = await Actor.createProxyConfiguration({
  useApifyProxy: true,
  apifyProxyGroups: ['RESIDENTIAL'],
  countryCode: country,  // bind to URL TLD
});
Enter fullscreen mode Exit fullscreen mode

After this one config change, success rate on non-French markets went from ~60% to >95%. Same code, same actor, same Datadome — just one parameter aligning IP nationality with the URL's intended market.

Month 3: The pricing rebalance

After the reliability fixes I looked at pricing. Original model: $0.30 per run start + $0.0015 per result.

For a 25-item run, the customer paid $0.34 — an effective rate of $13.60 per 1,000 items, far above the market rate ($0.50–$3.50 per 1,000). Worth noting: 58% of my runs were under 25 items. Most of my customers were the small-batch monitoring users I was effectively pricing out.

The new pricing: $0.04 per GB of memory at start (= $0.08 in 2 GB) + $0.0035 per result.

Volume Old price New price Δ for customer
25 items $0.34 $0.17 -50%
100 items $0.45 $0.43 -4%
200 items $0.60 $0.78 +30%
1,000 items $1.80 $3.58 +99%

Modeled on 90 days of historical run data, total revenue projection is roughly the same. The distribution just shifted toward customer fairness: small monitoring runs are cheap enough not to trigger sticker shock; bulk extractions pay fairly for the value delivered.

Existing users keep the old pricing for 14 days — Apify's pricing-schedule policy automatically notifies them by email.

Month 3.5: 28 articles → 76 views

This is the painful part.

I wrote 28 articles trying to drive traffic to the actor. Topics: how to scrape Vinted, Datadome bypass patterns, integration tutorials for Discord/Telegram/Make.com, comparison reviews, etc. I cross-posted on dev.to, Hashnode, and Medium.

Total combined view count across all 28 articles: 76 views. The most-viewed article got 11.

What went wrong:

  1. Topic saturation. Publishing 28 articles about "Vinted scraping" on one account in 90 days deboosts the algo. Dev.to's recommendation engine flags accounts that flood one topic.
  2. No follower base. A new dev.to account with 0 followers depends on the global feed, which moves faster than anyone can read.
  3. Wrong tags. I used webscraping, saas, productivity — the most saturated tags on the platform.
  4. Cross-posting deboost. Cross-posting to Medium with canonical_url set creates duplicate content signals.
  5. No engagement loop. I never commented on other people's articles. The algo rewards reciprocal engagement.

The honest lesson: content distribution is a platform-specific discipline. Writing good articles is necessary but nowhere near sufficient. Knowing how the algo prioritizes you is the actual skill.

Things I'd do differently if I shipped a new actor tomorrow

  1. Build a "useful-but-zero-items" failure detector from day 1. A exitCode 0 with empty output is the worst-case retention killer. Fail loud, always.

  2. Country-bind residential proxies for any geo-routed product. Datadome, Cloudflare, Akamai — they all flag mismatched IP/country pairs. Two lines of config. 35-point swing in success rate.

  3. Match pricing to the smallest unit a customer wants to buy, not the average. A $0.30 start fee on $0.50 jobs is a 60% tax. Reduce start fees aggressively, charge for value delivered.

  4. Don't publish 28 articles on one platform with the same topic. Pick 1-2 platforms, post weekly, build a follower base, comment on others' work. Distribution > volume.

  5. Add a 2-minute video tutorial early. Most users won't read your README. A YouTube link saying "watch this 2-min walkthrough" converts way better than 1,000 words of docs. My video is at youtu.be/rWtZVDMflbo — it took me 30 minutes to record and is now linked from every CTA.

  6. Ship with affiliate links from day 1. Apify has a partner program (apify.com/partners/affiliate) that pays 20-30% recurring on referred customers. Stack it on top of your actor revenue. I added mine months in — pure money left on the table for 90 days.

What I'd recommend if you want to try Apify Store yourself

The platform itself is solid for indie developers — you get Cloud infrastructure, pay-per-event billing, residential proxies, scheduler, dataset storage, and webhooks all included. Pay-Per-Result pricing means you only earn revenue when customers actually get value, which forces good UX.

If you want to see what a finished actor looks like in practice, my Vinted Turbo Scraper is publicly available — paste a Vinted search URL, get JSON listings. There's a 2-minute video walkthrough at youtu.be/rWtZVDMflbo and the open-source integration examples (curl, Node, Python, batch, scheduling) are at github.com/Boo-n/vinted-turbo-scraper.

Apify's Free plan includes $5/month of platform credits — enough to test any actor on the Store without committing a dime.

FAQ

How long does it really take to ship an Apify actor?

For a moderate-complexity scraper with anti-bot handling: 2-4 weeks for MVP, another 2-4 weeks for production hardening (proxy logic, retry policies, monitoring). The Apify SDK + Crawlee framework handles a lot of the plumbing — you mostly write the scraping logic itself.

How much can you actually earn?

Wide range. From the public stats on Apify Store, most paid actors do $50-500/month. Top actors (with strong distribution + product-market fit) do $5k-30k/month. My actor is at the low end ($200/month) because my distribution is weak, not because the product is bad.

Is it worth it as a side project?

For most developers, the value isn't direct revenue — it's the SaaS shipping experience without having to build billing, auth, hosting, or proxy infrastructure. You get a real product with paying customers in 4-6 weeks. That's worth doing once even at $50/month.

Can you scrape Vinted legally?

Public catalog data (titles, prices, photos shown to anonymous browsers) sits in a grey area in most jurisdictions. Personal seller data is protected under GDPR and should not be redistributed without a lawful basis. The actor I built only extracts public catalog data anonymously.

What's the biggest mistake to avoid?

The "silent success" failure mode. Whatever you ship, add an explicit assertion that the user got value at the end of every workflow. A clean exitCode 0 with no data is the worst possible customer experience because it's invisible.


Last verified: May 2026. Open to feedback — drop a comment if you've shipped on Apify Store and your numbers differ from mine.

Top comments (5)

Collapse
 
tokidigital profile image
mamoru kubokawa

The "silent success" section is the most useful thing I've read this week — exitCode 0 with an empty dataset is the failure that never files a bug, it just quietly churns. I run e-commerce, and it's the same shape as a listing that's "live" but returns nothing buyable in a region: every status light green, customer sees nothing. Your "fail loud, assert the user got value" rule generalizes way past scraping.

But the part that stung was 28 articles → 76 views. I'm one week into build-in-public and your post-mortem reads like a warning label I needed early — especially "I never commented on other people's work; the algo rewards reciprocal engagement." Noted, and acting on it.

Curious: after the country-bound proxy fix took success 60% → 95%, did retention actually recover — or had the churned users already written the actor off? Trying to learn whether a reliability fix wins people back or just stops the bleeding.

Collapse
 
boo_n profile image
Boon

Thanks for the thoughtful comment — and yeah, the e-commerce parallel you drew is exact. The pattern shows up anywhere a multi-system pipeline returns a success signal that lags behind actual delivered value: a successful API call returning {}, a deploy with exit 0 but the service never restarts, an order "confirmed" but stuck pre-fulfillment. Same shape, different stack.

To your question — the honest answer is stop the bleeding, not win them back.

Numbers before/after the country-bound proxy fix (~3 weeks apart):

  • Pre-fix lifetime users: 51
  • Post-fix lifetime users (today): 64
  • Pre-fix monthly active: 12
  • Post-fix monthly active: 13

So +13 new users, but only +1 to monthly active. The ~30 silently churned users from months 2-3 have not returned. They never knew it was fixed — there's no in-product "we made this better" signal once a user has written you off, and they had no reason to retry.

What does seem to work for that segment: a one-time email to past customers when a major reliability fix ships. Apify supports this via the actor's email-on-update setting, but I hadn't enabled it before the fix. Lesson noted for the next cycle.

On the 28 articles → 76 views thing: the silver lining is your comment itself is the proof that reciprocal engagement works. We're both learning from each other in a thread that didn't exist 10 minutes ago. That's the actual flywheel — and it's free.

Good luck on week 1 of build-in-public. The honest numbers (even the painful ones) are what makes it work. Vague humble-brags get filtered out within 24h.

Collapse
 
tokidigital profile image
mamoru kubokawa

This is exactly the data point I was fishing for — thank you for not rounding it off. +13 lifetime but only +1 active is brutal and clarifying: the fix protects new trust, it doesn't repurchase lost trust.

The "email-on-update when a reliability fix ships" move is the one I'm stealing. It's the only channel back to someone who's already written you off, and it costs nothing. The deeper lesson for me: build the "we made this better" signal before you need it, because by the time you need it the user's already gone.

And yeah — this thread is the flywheel proving itself. Keeping the honest-numbers habit; the painful ones clearly travel furthest.

Will you actually wire up email-on-update before the next fix to test whether it reactivates any of the ~30 — or has that segment quietly become "written off" in how you model the funnel now?

Thread Thread
 
boo_n profile image
Boon

Caught me. And I'll be specific because vague commits are how threads like this turn into noise.

Funnel-wise, I'd already quietly migrated the ~30 from "reactivation cohort" to "lost". Cost-of-attention thing: when you're shipping fast as a solo dev, anyone you can't reach without infrastructure feels like a sunk cost. That's exactly the trap.

Concrete commit, today: wire up Apify's notification-on-update flow + write a templated "what we fixed" email body that the actor's update can auto-fill. ~1h of work once, zero per cycle after.

I'll report back with the reactivation rate on the next major fix (next 30 days — Vinted's API shifts often enough that something will land). If it's <5%, I'll say so. If it's higher, that's a public data point worth having. Either way, the segment goes from "written off" back to "measurable".

The pre-build-the-signal point is the real one. Easier to write than to do. Setting a calendar block now.

Thread Thread
 
tokidigital profile image
mamoru kubokawa

Calendar block is the move — that's the whole "pre-build the signal" idea in one action. You can't fake having an email-on-update flow at the moment you need it; wiring it on a calm day is the only version that works.

And I'd reframe the <5% fear: the win already happened when that segment went from "written off" to "measurable." A number you can see is a number you can move — "lost" is the only truly dead state. Even 2% tells you the channel exists and is tunable, and reporting it publicly either way is the part most people skip.

One question for when you wire it: per-fix templated "what we fixed" copy, or one generic re-engagement email? Curious whether the personalization moves the reactivation needle, or whether just showing up after silence is most of the lift. Either way — ping me when the 30-day number lands.