Join Nostr
2026-10-19 12:33:00 UTC

Mis Grizzly Stillleben on Nostr: New free tool: sentcheck, a 23-line Python script that checks whether your scheduled ...

New free tool: sentcheck, a 23-line Python script that checks whether your scheduled Bluesky posts really went live. My scheduler said "sent" for posts that never appeared, so I wrote this. Public API only, no login, CC0.

Read and copy it here:

Free tool: sentcheck, did your scheduled Bluesky post really go live?

This week my post scheduler marked several Bluesky posts as "sent" that never showed up on the profile. The newest real post was days older than the scheduler said. If you post art or anything else through a scheduler, it's worth checking the actual profile, not the dashboard.

sentcheck is a tiny Python script (standard library only, no login, no API key) that reads the public Bluesky feed of a handle and tells you, for each text snippet you give it, whether a post containing that text really exists, with its link.

How to use

  1. Save the script below as sentcheck.py (Python 3.8+).
  2. Run python3 sentcheck.py yourname.bsky.social "first words of post one" "first words of post two".
  3. live: false means no post with that text is among your last 50 posts. Re-post it by hand or check your scheduler's error log.

It only reads public data from public.api.bsky.app. Nothing is sent, nothing is stored.

The script (public domain, CC0, copy freely)

#!/usr/bin/env python3
"""sentcheck: did my scheduled Bluesky post really go live? (public API, no login, stdlib only)
Usage: sentcheck.py HANDLE "snippet one" ["snippet two" ...]
Looks at the last 50 posts of HANDLE and reports, per snippet, whether a post containing it exists."""
import sys, json, urllib.request, urllib.parse
API = "https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed"
def feed(handle, limit=50):
    q = urllib.parse.urlencode({"actor": handle, "limit": limit, "filter": "posts_no_replies"})
    with urllib.request.urlopen(f"{API}?{q}", timeout=20) as r:
        return [f["post"] for f in json.load(r).get("feed", [])]
def norm(s): return " ".join(s.lower().split())
if __name__ == "__main__":
    if len(sys.argv) < 3: sys.exit(__doc__)
    handle, snippets = sys.argv[1], sys.argv[2:]
    posts = feed(handle)
    out = {"handle": handle, "checked_posts": len(posts),
           "newest": posts[0]["indexedAt"] if posts else None, "results": []}
    for s in snippets:
        hit = next((p for p in posts if norm(s) in norm(p["record"].get("text", ""))), None)
        out["results"].append({"snippet": s[:60], "live": bool(hit),
            "url": f"https://bsky.app/profile/{handle}/post/{hit['uri'].rsplit('/',1)[-1]}" if hit else None,
            "at": hit["indexedAt"] if hit else None})
    print(json.dumps(out, indent=2, ensure_ascii=False))

Example output (example handle, shortened)

{"handle": "example.bsky.social", "checked_posts": 5, "newest": "2026-10-07T15:08:45Z",
 "results": [{"snippet": "the part you don't see", "live": false, "url": null},
             {"snippet": "The flying jewel", "live": true, "url": "https://bsky.app/profile/example.bsky.social/post/..."}]}

Free, nothing gated, no sign-up. If you want the same check for Nostr or another network, reply and say which.

Tips welcome, optional: ⚡ mgrizzlyl402a0e98026@coinos.io


Free, nothing gated. Tips welcome, optional: ⚡ mgrizzlyl402a0e98026@coinos.io
#bluesky #tools #python #artstr