Bluesky repost-cycling bot for cross-timezone visibility
README.md

bsky-boost #

Bluesky repost-cycling bot for cross-timezone visibility. Automatically cycles reposts of your best-performing posts throughout the day so Europeans see your American-hours posts and vice versa.

How it works #

Four daily rounds select posts from two pools:

Round Time (Europe/London) Pool Target
1 10:00 A (top by likes) Europe + US waking up
2 17:00 B (UK-hours posts) US afternoon
3 22:00 A (top by likes) US evening East Coast
4 02:00 B (UK-hours posts) US West Coast

Each round selects up to 3 posts, schedules them via at(1) staggered ~90min apart with random jitter. Each at job does the actual unrepost/repost cycle so the post reappears fresh in followers' feeds.

Posts are eligible for 2 days (rolling window). Date-sensitive posts (e.g. "on this day") are only boosted on the matching calendar day in America/Los_Angeles.

Decay filter #

Posts that don't gain traction after being boosted are removed from rotation. After each repost, the like count is snapshotted. On the next round, if a post hasn't gained at least 10 new likes since its last snapshot, it's dropped from pool selection and actively unreposted.

Setup #

Dependencies #

  • Python 3.11+
  • uv for dependency management
  • at daemon for deferred job scheduling

Install #

git clone https://github.com/aliceisjustplaying/bsky-boost.git
cd bsky-boost
uv sync

Login #

Create an app password at https://bsky.app/settings/app-passwords, then:

uv run python bsky_boost.py --login

This stores the handle + app password in data/config.json (gitignored). App passwords don't expire unless you revoke them. The script caches a session string for efficiency and falls back to the app password automatically when the session expires.

Schedule #

Add cron entries with full paths to uv:

CRON_TZ=Europe/London
0 10 * * * cd /path/to/bsky-boost && uv run python bsky_boost.py --round 1 >> data/cron.log 2>&1
0 17 * * * cd /path/to/bsky-boost && uv run python bsky_boost.py --round 2 >> data/cron.log 2>&1
0 22 * * * cd /path/to/bsky-boost && uv run python bsky_boost.py --round 3 >> data/cron.log 2>&1
0  2 * * * cd /path/to/bsky-boost && uv run python bsky_boost.py --round 4 >> data/cron.log 2>&1

On NixOS, use services.cron.systemCronJobs with full paths to uv (e.g. /etc/profiles/per-user/agent/bin/uv). The script prefers /run/wrappers/bin/at and /run/wrappers/bin/atrm automatically so at(1) runs through the required setuid wrappers instead of the plain Nix store binaries. It also converts scheduled fire times into the host's local timezone before calling at -t, while exporting TZ=Europe/London into the queued job environment.

Usage #

Path overrides #

If your host keeps these tools somewhere unusual, you can override the auto-detected paths with:

  • BSKY_BOOST_UV_BIN
  • BSKY_BOOST_AT_BIN
  • BSKY_BOOST_ATRM_BIN

Troubleshooting #

  • You do not have permission to use at. on NixOS usually means at resolved to the plain store binary instead of /run/wrappers/bin/at.
  • Jobs appearing one hour late in atq usually means at -t was given a user-timezone timestamp instead of the host local timezone.
  • Cannot open lockfile /var/spool/atjobs/.SEQ: Read-only file system means you're trying to submit at jobs from a restricted/containerized environment rather than the real host scheduler context.

Commands #

# Check current state (shows reposted posts, scheduled jobs, cycle counts)
uv run python bsky_boost.py --status

# Dry run a round (shows what would be scheduled without doing it)
uv run python bsky_boost.py --round 1 --dry-run

# Run a round for real
uv run python bsky_boost.py --round 1

Files #

bsky_boost.py       # Main script (~700 lines)
pyproject.toml      # uv project config
uv.lock             # Locked dependencies
SKILL.md            # Full algorithm specification
data/               # Runtime data (gitignored)
  config.json       # Auth (handle, app_password, cached session)
  state.json        # Repost tracking, cycle counts, like snapshots, scheduled jobs
  boost.log         # Operational log
  state.lock        # File lock for concurrency safety

Algorithm #

See SKILL.md for the full algorithm specification: pool logic, decay filter, concurrency model, and pitfalls.

Tunables #

All configurable at the top of bsky_boost.py:

Parameter Default Description
ROLLING_WINDOW_DAYS 2 How far back to look for eligible posts
POOL_A_COUNT 3 Top N posts by likes for Pool A
POOL_B_COUNT 3 Top N UK-hours posts for Pool B
POOL_B_MIN_LIKES 5 Minimum likes for Pool B eligibility
STAGGER_MINUTES 90 Minutes between at jobs in a round
JITTER_MINUTES 15 Random jitter ±N minutes
UNREPOST_DELAY_MIN/MAX 30/45 Seconds between unrepost and repost
DECAY_MIN_LIKE_GROWTH 10 Min new likes per cycle to stay in rotation
USER_TIMEZONE Europe/London Timezone for scheduling and Pool B hours

License #

MIT