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
atdaemon 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_BINBSKY_BOOST_AT_BINBSKY_BOOST_ATRM_BIN
Troubleshooting #
You do not have permission to use at.on NixOS usually meansatresolved to the plain store binary instead of/run/wrappers/bin/at.- Jobs appearing one hour late in
atqusually meansat -twas given a user-timezone timestamp instead of the host local timezone. Cannot open lockfile /var/spool/atjobs/.SEQ: Read-only file systemmeans you're trying to submitatjobs 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