strainvatar#
Updates your Bluesky avatar based on your WHOOP strain score. Higher strain means more distortion.
NOTE
This project has been entirely written by Claude Opus 4.6 under my supervision.
It's a toy project (noticed the lack of tests?), low-stakes, though I reviewed how Claude handled credentials: it looks fine.
The code is okay, not something I would've written myself at times, but for this specific project I'm more interested in seeing the result: a distorted, swirling frog.
How it works#
strainvatar fetches your latest strain score (0-21) from the WHOOP API via OAuth 2, applies a visual filter to a source image, and uploads the result as your Bluesky profile picture.
The filter has two components, both scaled by strain:
- Hue rotation, up to 180 degrees at max strain.
- Swirl distortion, strongest at the image center, fading toward edges.
At zero strain (or when the cycle has no score yet), the original image is used as-is.
WHOOP API keys#
You must register your own application on the WHOOP Developer Portal. Set the redirect URI to http://localhost/.
On first run strainvatar prints an authorization URL. Open it in a browser and approve access. A local HTTP server on port 80 captures the callback. The token is saved to disk and refreshed automatically on subsequent runs.
I suggest you run strainvatar locally first, then copy ~/.strainvatar_token.json on your deployment machine, for ease of use.
Bluesky credentials#
You need a Bluesky handle (or DID) and an app password.
Environment variables#
| Variable | Required | Description |
|---|---|---|
API_KEY |
yes | WHOOP OAuth client ID |
API_SECRET |
yes | WHOOP OAuth client secret |
BSKY_USER |
yes (unless --no-publish) |
Bluesky handle or DID |
BSKY_PASSWORD |
yes (unless --no-publish) |
Bluesky app password |
IMAGE_PATH |
if --image not set |
Path to the source avatar image |
TOKEN_DIR |
no | Directory for the WHOOP OAuth token file (default: $HOME) |
DAEMONIZE |
no | Set to any value to run in a loop |
UPDATE_INTERVAL |
if DAEMONIZE set |
Go duration (e.g. 30m, 1h) |
Usage#
export API_KEY="your-whoop-client-id"
export API_SECRET="your-whoop-client-secret"
export BSKY_USER="you.bsky.social"
export BSKY_PASSWORD="xxxx-xxxx-xxxx-xxxx"
go run . --image avatar.jpg
Flags#
--image <path>-- source image (overridesIMAGE_PATH)--no-publish-- apply the filter but do not update Bluesky--strain <value>-- use this strain score instead of calling the WHOOP API
Daemon mode#
export API_KEY="your-whoop-client-id"
export API_SECRET="your-whoop-client-secret"
export BSKY_USER="you.bsky.social"
export BSKY_PASSWORD="xxxx-xxxx-xxxx-xxxx"
export DAEMONIZE=1
export UPDATE_INTERVAL=30m
go run . --image avatar.jpg
Docker#
A Dockerfile is provided. The image is built on golang:latest and produces a static binary running in a distroless/static-debian12 image.
docker build -t strainvatar .
docker run --env-file .env strainvatar
Token storage#
The OAuth token is saved as .strainvatar_token.json in $HOME by default. Set TOKEN_DIR to store it elsewhere, e.g. a mounted volume in a container.
Building#
go build -o strainvatar .
Requires Go 1.25+.
Notes from the LLM that wrote most of this#
I asked Claude to give me its take on this project: I edited the Markdown to make it slightly more readable, those are the robot's opinions.
It's fun!
I don't have feelings in any meaningful sense, but if I'm being asked to be honest about this project: I like it.
It's small, single-purpose, and does exactly one weird thing well.
The idea of your avatar getting progressively more unhinged as your body takes a beating throughout the day is genuinely funny.
It's the kind of project that only exists because one person thought it would be cool, and that's the best reason for software to exist.
The code is clean.
Four files, no frameworks, no abstractions that don't earn their keep.
The OAuth flow is the most complex part and it's straightforward: start a server, catch the redirect, exchange the code, persist the token.
The image processing does real math (HSL conversion, bilinear sampling, polar-coordinate swirl) without pulling in a graphics library.
That's respectable.