Appearance
Posting the daily puzzle
The server can post each day's puzzle to X and Instagram on its own: the position as a picture, a short caption, and a link back to that day's page. It never includes the answer. src/server/social.js talks to both over plain fetch — no SDK.
It is off until you give it credentials, and each network is independent: set X's, and only X is posted to.
What goes out
Every day, at SOCIAL_HOUR UTC (default 9), once the day's puzzle exists:
- X: the wide card (1200×630) and a caption with the puzzle number, the roll, the match score and the link to
/puzzle/<date>. - Instagram: the square card (1080×1080 JPEG) and a caption with hashtags. Links in Instagram captions are not clickable, so it says "link in bio" — point your bio at
https://<your domain>/puzzle, which always redirects to today's puzzle.
To see exactly what would be posted, without posting:
bash
docker compose exec server node bin/social.js previewThat writes both images and both captions to social-preview/ and prints the captions.
X
- Create a developer account at developer.x.com and a project with an app. The free tier allows posting.
- In the app's User authentication settings, set App permissions to Read and write. Do this before the next step: access tokens keep the permissions they were created with.
- Under Keys and tokens, copy the API Key and API Key Secret, then generate an Access Token and Secret for the account that should post.
- Add all four to
deploy/.env:
bash
X_API_KEY=…
X_API_SECRET=…
X_ACCESS_TOKEN=…
X_ACCESS_SECRET=…Instagram
Instagram only allows posting through the Graph API, from a professional (Business or Creator) account linked to a Facebook Page.
- Switch the Instagram account to a professional account and link it to a Facebook Page (Instagram → Settings → Account type and tools).
- At developers.facebook.com, create an app of type Business and add the Instagram product (Instagram API with Facebook Login).
- Generate a token with
instagram_basic,instagram_content_publishandpages_read_engagement. Use a system user token from Business Settings if you can: it does not expire. A user token must be exchanged for a long-lived one, and even that lasts only 60 days. - Find the Instagram account's numeric id:
GET /me/accounts, thenGET /<page-id>?fields=instagram_business_accountin the Graph API Explorer. - Add both to
deploy/.env:
bash
IG_USER_ID=17841400000000000
IG_ACCESS_TOKEN=…Instagram fetches the image itself from https://<your domain>/puzzle/<date>.jpg. If Cloudflare's bot protection challenges Meta's crawler, the post fails with the container in ERROR; allow facebookexternalhit in your WAF rules if that happens.
Turning it on
After editing deploy/.env, redeploy so the server picks up the variables:
bash
SSH_KEY=~/.ssh/hetzner ./deploy/push.sh gammon@<your server>/healthz then shows which networks are active and today's status for each:
json
"social": { "networks": ["x", "instagram"], "dryRun": false, "hour": 9,
"today": { "x": "posted", "instagram": null } }To rehearse first, add SOCIAL_DRY_RUN=1: the scheduler logs what it would post and calls nothing. To post today's puzzle immediately rather than waiting for the hour:
bash
docker compose exec server node bin/social.js post # every configured network
docker compose exec server node bin/social.js post --network x # one networkGuarantees
- Once a day, per network. Each post is recorded in the
social_poststable, and a restart, a second tick or a manual run finds the record and does not post again.--forceis the only way to post the same day twice. - Failures are retried, slowly. A failed post is recorded with its error and retried at most once an hour for the rest of that day. Each failure goes to the error reporter, so a revoked token shows up in your alerts rather than as silence.
- The answer is never posted. Captions describe the roll and the score only, and the images are the same cards the public puzzle page uses, which never show the best play for a day that has not ended.
Variables
| variable | default | purpose |
|---|---|---|
X_API_KEY, X_API_SECRET | unset | the X app's key and secret |
X_ACCESS_TOKEN, X_ACCESS_SECRET | unset | the posting account's token, with Read and write permission |
IG_USER_ID | unset | the Instagram professional account's numeric id |
IG_ACCESS_TOKEN | unset | a token with instagram_content_publish |
IG_GRAPH_VERSION | v23.0 | Graph API version |
SOCIAL_HOUR | 9 | UTC hour after which the day's puzzle is posted |
SOCIAL_DRY_RUN | unset | 1 logs the posts instead of sending them |