Building a Bluesky Post Rate API in Two AppView Calls

Bluesky users call their posts skeets, and I wanted a quick way to answer a silly question with real data: how many skeets per day does a given account actually post? So nerdymark.com's API grew a new endpoint, GET /api/skeets-per-day, and a friendly front end to go with it. The whole feature was designed around one constraint: a lookup has to cost almost nothing, even if the tool goes viral.

The constraint: survive success

If you build a stats tool and it takes off, the invocation count is out of your hands. The naive implementation pages through app.bsky.feed.getAuthorFeed and counts everything. At 100 posts per page, a 50,000-post account costs 500 upstream calls for a single pageview. That math fails the moment two people share the link, so the rule going in was simple: if the stat can't be computed in a tiny, fixed number of calls, it isn't worth offering.

Two AppView calls, bounded forever

It turns out Bluesky hands you the lifetime answer for free. app.bsky.actor.getProfile already returns postsCount, which counts posts and replies (reposts and likes are separate record types), plus the account's createdAt. Divide one by the other and you have the lifetime average in exactly one call, whether the account has 50 skeets or 500,000.

A lifetime average hides hot streaks and hibernation, though, so the endpoint spends one more call: a single page of getAuthorFeed, newest 100 posts and replies. Measure the time span from the oldest sampled post to now, divide, and you get the recent pace. A prolific account burns through 100 posts in a couple of days, so its recent window is tight. A quiet account's newest 100 posts might span months. The response includes window_days so consumers can tell which case they're looking at, and the cost stays at one page no matter how active the account is.

Self-defense layers

Both calls hit the unauthenticated public AppView at public.api.bsky.app, so no session tokens are spent and the per-IP limits are generous. In front of that sits a one-hour in-memory cache, which handles the viral case beautifully: when everyone checks the same celebrity, only the first lookup touches Bluesky. And flask-limiter caps the route itself at 30 requests per minute per client, so nobody can use my server as a Bluesky proxy.

Try it

The interactive version lives at the skeets per day calculator, which also explains the methodology. Developers can hit the JSON directly:

curl "https://nerdymark.com/api/skeets-per-day?actor=nerdymark.com"

{
  "handle": "nerdymark.com",
  "total_skeets": 3238,
  "account_age_days": 630.5,
  "skeets_per_day": 5.14,
  "recent": {
    "sample_size": 100,
    "window_days": 7.6,
    "skeets_per_day": 13.22
  }
}

Full parameter and error documentation is in the API docs. Deleted posts don't count, brand-new accounts are floored at one day of age so the rates stay sane, and results can lag up to an hour thanks to the cache. Apparently I post 13 skeets a day lately, which is about 2.5 times my lifetime average. No further questions.

More in Technology