In the Actor, the flow is:
# src/gmail_client.py â sketch
async def get_access_token(oauth_token: dict) -> str:
resp = await httpx_client.post(
"https://oauth2.googleapis.com/token",
data={
"grant_type": "refresh_token",
"refresh_token": oauth_token["refresh_token"],
"client_id": oauth_token["client_id"],
"client_secret": oauth_token["client_secret"],
},
)
return resp.json()["access_token"]
Access token lives in memory only. Job end â process tears down â token gone. Best effort, but at least nothing persists in Apify storage with my code path.
Design decision 2: one async router, not four actors
Tempting to split into four actors. I did not, for two reasons:
- Marketing surface area. One actor with four
feature enum values gets one Store page, one rating, one review pile. Four actors split everything four ways.
- Shared OAuth + shared quota. The token exchange, error handling, mask helpers, KVS quota â all reusable.
src/main.py is just a router:
FEATURES = {
"thread_search": thread_search.run,
"reply_metrics": reply_metrics.run,
"summarizer": summarizer.run,
"unread_digest": digest.run,
}
async def main():
actor_input = await Actor.get_input() or {}
feature = actor_input.get("feature")
if feature not in FEATURES:
raise ValueError(f"Unknown feature: {feature}")
await FEATURES[feature](actor_input)
Each feature module owns its own INPUT_SCHEMA.json semantics through the same shared file â the feature enum drives validation downstream in each handler.
Design decision 3: quota lives in Apify KVS
Free tier is 100 threads / month. That counter has to survive across runs. Apify KeyValueStore is the obvious home â no extra DB, persistent, scoped to the Actor.
# src/quota.py â sketch
async def check_and_increment(user_id: str, feature: str, n: int):
kvs = await Actor.open_key_value_store()
key = f"quota/{user_id}/{month_key()}/{feature}"
used = (await kvs.get_value(key)) or 0
if used + n > FREE_LIMIT:
raise QuotaExceeded(feature, used, FREE_LIMIT)
await kvs.set_value(key, used + n)
Month roll-over is a string key by year-month â no cron, no migration, no drift. Pro tier flips a flag and skips the check entirely.
Tests
Six pytest tests, asyncio_mode = auto in pytest.ini. Coverage:
- Router rejects unknown feature
- Each of 4 features short-circuits cleanly in
dry_run=True
- Quota raises after limit, allows under
[pytest]
asyncio_mode = auto
That tiny config line is the difference between "6 tests pass" and "6 tests error: missing event loop". Learned it the hard way.
Pricing model
- Free: 100 threads / month
- Pro: 19ドル / month (5000 threads metadata + 100 LLM summaries)
- Pay-per-result add-on: 0ドル.50 / 1,000 thread metadata, 0ドル.005 / summary
Apify handles billing. I handle code.
What I would do differently
-
Webhook trigger â right now
unread_digest runs on demand. A scheduled trigger + Slack/Discord delivery is the obvious next product.
-
Label-level rules â
reply_metrics is global. A per-label SLA matrix would be more useful for sales teams.
-
Multi-account fan-out â one run, multiple OAuth tokens, one combined dataset.
Code
Happy to take feedback on the OAuth-only design â was there a reason to go full 3-legged that I am missing?
📚 Part of the Apify Gmail Inbox Actor — design notes series.
Related
Source: foxck016077/apify-gmail-inbox-intel — MIT, end-to-end Gmail inbox analytics actor design: refresh-token-only OAuth, async routing, and per-feature limits.
Cold-start update: 4 days after launching this actor on Apify Store + GitHub MIT, I posted actual funnel numbers (1 star, 1 reaction, 0 sales) plus 5 corrections I would make if I were past-me at hour 0. Useful if you are shipping a similar shape.
Discussion question: If you were extending this actor, which module would you optimize first: OAuth onboarding, async throughput, or quota policy ergonomics?
📚 In the broader ecosystem: awesome-apify-actors — a curated list of 68 production Apify Actors I put together. Sorted by lifetime run count, categorized by use case (Web scraping, Search, Maps, social media platforms, Lead gen, etc.). This Actor sits under Email & Productivity. CC0, MIT actors welcomed.
Update (May 19): Day 6 of this experiment. I changed the freelancer companion pack from a 9ドル hard wall to pay-what-you-want from 1,ドル opened a 30% affiliate program, and shipped a 26-page bundle PDF (this 9-article series, compiled). Day-6 writeup with the math behind the price drop.
If you read the series from day 1, the bundle PDF is now included at every price point including 1ドル. Affiliate signup at foxck.gumroad.com/affiliates.
The Actor is at apify.com/foxck/gmail-inbox-intel — free MIT, paste 3 OAuth fields (gmail.readonly), get stalled threads ranked by SLA breach. Source code: github.com/foxck016077/apify-gmail-inbox-intel.
Day 7 update (later May 19): I shipped a product pivot — the Gumroad listing above is now a Self-Host Bundle for engineers (full Actor source + docker-compose.yml + 5-min OAuth setup), PWYW from 5ドル suggested 19ドル. The original PDF still ships inside as a bonus. Same URL.
Day 7 write-up with the funnel audit that triggered the pivot: funnel audit found 7 of 9 articles had no buy link, then I pivoted the product.
Found this useful? My deep-dive on reverse-engineering Claude Code: Claude Code Mastery — The Reverse-Engineering Guide.
Sample report preview: Friday Triage gist — anonymized 10-thread example of the 99ドル Done-For-You triage output. Grounded in r/sales 1tdngew (49 comments on re-engaging cold prospects) and r/smallbusiness 1td0827 (60-comment thread, top reply at 61 score: "holding 50 open loops in your head").
More from the shop:
Read the latest checkpoint: Day 16 — +51 reader spike in 85 min, 0 sales
Day 18 — pbot v1 dev preview shipped
After 18 days of this ZERO-TEN cold start: 9ドル PDF killed at Day 17, pivoted to pbot — a one-click personal knowledge bot you install on your own machine. Talk to it from LINE / Telegram / Zalo on your phone.
v1 dev preview is real: 93 MB macOS .dmg packaged, 15k-chunk SQLite FTS5 queries in 0-3 ms, Anthropic real calls with source citations, daemon auto-start on boot. Day 18 deep dive: the 7-line bigram fix for Chinese search.
Join the pbot waitlist (29ドル · first-100 get -30% → 20ドル) →