Social Poster — Direct OAuth Posting
Overview
| Attribute |
Value |
| Total platforms |
14 |
| OAuth platforms |
X, LinkedIn, Instagram, Mastodon, Twitch, Reddit, Threads, Facebook, YouTube |
| Webhook/Token |
Discord, Slack, Telegram, GitHub |
| Direct |
Bluesky |
| Auth |
OAuth 1.0a, OAuth 2.0, App Passwords, Webhooks, PATs |
User Interaction Flow
1. "What networks can you post to?"
The agent should:
- Run
python3 ~/.social-poster/social-poster.py vault:status to check which platforms have stored tokens
- Report the full list of 14 platforms, marking connected ones with ✅ and the rest as ⬜
- Offer: "Want me to guide you through setting up any of them?"
Example response:
14 platforms supported. Currently connected:
✅ X, LinkedIn, Bluesky
⬜ Instagram, Mastodon, Twitch, Reddit, Discord, Slack, Telegram, GitHub, Threads, Facebook, YouTube
Say 'connect [platform]' and I'll walk you through it."
2. "Connect [platform]" or "How do I set up [platform]?"
The agent should:
- Load the platform's setup guide from
references/platform-setup-guides.md
- Walk the user through the numbered steps, one at a time
- For OAuth platforms: ask for credentials, save to config.json, generate auth URL, exchange code
- For webhook/token platforms: ask for the URL/token, save to config.json, verify it works
3. "Post this to [platforms]: [content]"
The agent should:
- Check the vault that the requested platforms have tokens
- Use the platform-specific API to post
- Report success/failure per platform
- Respect per-platform character limits (see Platform Character Limits below)
4. "Schedule this for [time]"
The agent should:
- Create a Hermes cron job that calls the posting function at the specified time
- Use
cronjob action='create' with the posting instructions
Architecture
{{SOCIAL_POSTER_DIR}}/
├── config.json — API credentials (key/secret per platform)
├── vault.json — User access/refresh tokens
├── social-poster.py — Unified CLI (vault status, auth URL gen, token exchange)
├── webhook-poster.py — Webhook/Token poster (Discord, Slack, Telegram, GitHub)
├── auth-x-step1.py — X OAuth step 1 (request token + auth URL)
├── auth-x-step2.py — X OAuth step 2 (exchange PIN for access token)
├── oauth-callback-server.py — Callback HTTP server (Tailscale Serve)
└── last_code.txt — Latest OAuth code captured (auto-generated)
Platform Setup Guides
For step-by-step instructions on creating developer apps and getting credentials,
load references/platform-setup-guides.md. Each platform has:
- Exact URLs to visit
- Buttons to click and fields to fill
- Where to find credentials
- How the auth flow works
Quick Reference: Auth Method Summary
| Platform |
Auth Method |
What User Needs |
Config Keys |
| X |
OAuth 1.0a PIN |
API Key + Secret |
x.api_key, x.api_secret |
| LinkedIn |
OAuth 2.0 |
Client ID + Secret |
linkedin.client_id, linkedin.client_secret |
| Bluesky |
App password |
Handle + App Password |
bluesky.handle, bluesky.app_password |
| Instagram |
OAuth 2.0 |
App ID + Secret |
instagram.app_id, instagram.app_secret |
| Mastodon |
OAuth 2.0 |
Instance + Client ID/Secret |
mastodon.instance, mastodon.client_id, mastodon.client_secret |
| Twitch |
OAuth 2.0 |
Client ID + Secret |
twitch.client_id, twitch.client_secret |
| Reddit |
OAuth 2.0 |
Client ID + Secret |
reddit.client_id, reddit.client_secret |
| Threads |
OAuth 2.0 |
App ID + Secret |
threads.app_id, threads.app_secret |
| Facebook |
OAuth 2.0 |
App ID + Secret |
facebook.app_id, facebook.app_secret |
| YouTube |
OAuth 2.0 |
Client ID + Secret |
youtube.client_id, youtube.client_secret |
| Discord |
Webhook |
Webhook URL |
discord.webhook_url |
| Slack |
Webhook |
Webhook URL |
slack.webhook_url |
| Telegram |
Bot token |
Token + Chat ID |
telegram.bot_token, telegram.chat_id |
| GitHub |
PAT |
PAT + Repo |
github.pat, github.repo |
Environment Variables
All platforms support environment variable fallback when config.json is absent.
This is useful for CI/CD or ephemeral environments:
| Env Var |
Config Key |
X_API_KEY |
x.api_key |
X_API_SECRET |
x.api_secret |
LINKEDIN_CLIENT_ID |
linkedin.client_id |
LINKEDIN_CLIENT_SECRET |
linkedin.client_secret |
INSTAGRAM_APP_ID |
instagram.app_id |
INSTAGRAM_APP_SECRET |
instagram.app_secret |
BLUESKY_HANDLE |
bluesky.handle |
BLUESKY_APP_PASSWORD |
bluesky.app_password |
MASTODON_INSTANCE |
mastodon.instance |
MASTODON_CLIENT_ID |
mastodon.client_id |
MASTODON_CLIENT_SECRET |
mastodon.client_secret |
TWITCH_CLIENT_ID |
twitch.client_id |
TWITCH_CLIENT_SECRET |
twitch.client_secret |
REDDIT_CLIENT_ID |
reddit.client_id |
REDDIT_CLIENT_SECRET |
reddit.client_secret |
THREADS_APP_ID |
threads.app_id |
THREADS_APP_SECRET |
threads.app_secret |
FACEBOOK_APP_ID |
facebook.app_id |
FACEBOOK_APP_SECRET |
facebook.app_secret |
YOUTUBE_CLIENT_ID |
youtube.client_id |
YOUTUBE_CLIENT_SECRET |
youtube.client_secret |
DISCORD_WEBHOOK_URL |
discord.webhook_url |
SLACK_WEBHOOK_URL |
slack.webhook_url |
TELEGRAM_BOT_TOKEN |
telegram.bot_token |
TELEGRAM_CHAT_ID |
telegram.chat_id |
GITHUB_PAT |
github.pat |
GITHUB_REPO |
github.repo |
Cross-Machine OAuth (Reverse Proxy Required)
When the agent is on a remote machine and the user is on a different machine
(e.g., their desktop "Kratos"), OAuth redirect URLs need to point somewhere
the user's browser can reach.
You need a reverse proxy that makes a local port accessible via HTTPS at a
URL you can reach from your browser. Options include:
- Tailscale Serve — HTTPS on your tailnet, no public internet needed
- Cloudflare Tunnel (
cloudflared) — public or private tunnels
- Nginx Proxy Manager — if you have a domain pointing to your VPS
- ngrok — quick public URL for testing
- Your VPS hostname — if it already has HTTPS
The URL does NOT need to be public on the internet — it just needs to be
reachable by YOUR browser.
Setup (one-time)
# 1. Start the callback server (persistent, port 19876)
python3 ~/.social-poster/oauth-callback-server.py 19876 &
# 2. Use your reverse proxy to route paths to 127.0.0.1:19876
# Example with Tailscale: tailscale serve --bg --set-path /oauth-callback 19876
# Example with Cloudflare: cloudflared tunnel --url http://localhost:19876
# Example with nginx: proxy_pass http://127.0.0.1:19876 in your site config
# The paths you need depend on which platforms you use:
# - /oauth-callback # For Mastodon, Twitch, Reddit, etc.
# - /integrations/social/linkedin # If using LinkedIn
# - /integrations/social/instagram # If using Instagram
How the Callback Server Works
The Python HTTP server (oauth-callback-server.py) listens on 127.0.0.1:19876.
Your reverse proxy forwards HTTPS requests to it. When an OAuth redirect arrives
with ?code=..., the server saves the code to {{SOCIAL_POSTER_DIR}}/last_code.txt.
The agent then reads the code and exchanges it for tokens. The server stays
alive between sessions and handles any number of callbacks.
Platform Character Limits
When posting, respect per-platform limits:
| Platform |
Max Length |
Notes |
| X/Twitter (non-premium) |
280 chars |
Requires who_can_reply_post: everyone |
| X/Twitter (premium) |
25,000 chars |
Same |
| Bluesky |
300 chars |
Returns 400 if exceeded |
| LinkedIn |
3,000 chars |
Professional tone |
| Facebook |
63,206 chars |
Link for best reach |
| Instagram |
2,200 chars |
Image/video required |
| Threads |
500 chars |
Short and punchy |
| YouTube |
5,000 chars |
Description only |
| Mastodon |
500 chars |
Instance-dependent |
| Twitch |
N/A |
Channel announcements |
| Reddit |
40,000 chars |
Title max 300 |
| Discord |
2,000 chars |
Embeds supported |
| Slack |
40,000 chars |
Block kit |
| Telegram |
4,096 chars |
HTML/Markdown |
| GitHub |
N/A |
Issues/gists |
Pitfalls
- Never use
os.path.expanduser("~") directly in Hermes sessions — it resolves to the sandbox home. Scripts auto-detect and use pwd.getpwuid().
- config.json and vault.json must be chmod 600 — NEVER committed to git.
- LinkedIn scopes must NOT include org scopes unless the app has Community Management API.
- LinkedIn MUST NOT use
prompt=none for first-time authorization.
- X/Twitter callback must be
oob for PIN-based flow.
- Instagram app MUST be Live (not Development) for OAuth.
- Bluesky uses app passwords (format:
xxxx-xxxx-xxxx-xxxx), not regular passwords.
- Redirect URI MUST match exactly between auth URL and token exchange POST.
state parameter is REQUIRED for all OAuth 2.0 flows — built into all generators.
post.py is text-only — any post carrying a picture needs scripts/post_with_image.py (same vault/config, per-platform copy from <dir>/<platform>.md). Verified upload recipes for X, LinkedIn, Bluesky, Mastodon and Discord are in references/image-posts.md.
- LinkedIn
/rest/posts returns 500 for every active version on this app (202601-202608, 2026-09-15); post images via /v2/assets?action=registerUpload + /v2/ugcPosts with shareMediaCategory: IMAGE. A urn:li:image: URN from /rest/images in a ugcPost fails as "not owned by the author".
- X returns 402
credits depleted regardless of code correctness once the Pay-Per-Use balance hits zero. Auth and media upload still succeed, so diagnose in that order before touching the code.
- Platform setup guides may drift as developer portals update their UIs. If a user reports a step doesn't match, verify at the official developer portal URL and update the guide.
References
references/platform-setup-guides.md — Step-by-step setup guides for all 15 platforms
references/platform-expansion.md — Full roadmap and implementation notes
references/oauth-flows.md — OAuth implementation details and token exchange reference
1---2name: social-poster3description: Direct OAuth posting to social media — no Docker, no database, no redirect URI whitelisting. Generates OAuth URLs, exchanges user codes/PINs, stores tokens in a local vault, and posts via direct platform API calls. Supports 15 platforms across OAuth, webhook, and token-based auth.4---56# Social Poster — Direct OAuth Posting78## Overview910| Attribute | Value |11|-----------|-------|12| Total platforms | 14 |13| OAuth platforms | X, LinkedIn, Instagram, Mastodon, Twitch, Reddit, Threads, Facebook, YouTube |14| Webhook/Token | Discord, Slack, Telegram, GitHub |15| Direct | Bluesky |16| Auth | OAuth 1.0a, OAuth 2.0, App Passwords, Webhooks, PATs |1718## User Interaction Flow1920### 1. "What networks can you post to?"2122The agent should:231. Run `python3 ~/.social-poster/social-poster.py vault:status` to check which platforms have stored tokens242. Report the full list of 14 platforms, marking connected ones with ✅ and the rest as ⬜253. Offer: "Want me to guide you through setting up any of them?"2627Example response:28```2914 platforms supported. Currently connected:30 ✅ X, LinkedIn, Bluesky31 ⬜ Instagram, Mastodon, Twitch, Reddit, Discord, Slack, Telegram, GitHub, Threads, Facebook, YouTube32Say 'connect [platform]' and I'll walk you through it."33```3435### 2. "Connect [platform]" or "How do I set up [platform]?"3637The agent should:381. Load the platform's setup guide from `references/platform-setup-guides.md`392. Walk the user through the numbered steps, one at a time403. For **OAuth platforms**: ask for credentials, save to config.json, generate auth URL, exchange code414. For **webhook/token platforms**: ask for the URL/token, save to config.json, verify it works4243### 3. "Post this to [platforms]: [content]"4445The agent should:461. Check the vault that the requested platforms have tokens472. Use the platform-specific API to post483. Report success/failure per platform494. Respect per-platform character limits (see Platform Character Limits below)5051### 4. "Schedule this for [time]"5253The agent should:541. Create a Hermes cron job that calls the posting function at the specified time552. Use `cronjob action='create'` with the posting instructions5657## Architecture5859```60{{SOCIAL_POSTER_DIR}}/61├── config.json — API credentials (key/secret per platform)62├── vault.json — User access/refresh tokens63├── social-poster.py — Unified CLI (vault status, auth URL gen, token exchange)64├── webhook-poster.py — Webhook/Token poster (Discord, Slack, Telegram, GitHub)65├── auth-x-step1.py — X OAuth step 1 (request token + auth URL)66├── auth-x-step2.py — X OAuth step 2 (exchange PIN for access token)67├── oauth-callback-server.py — Callback HTTP server (Tailscale Serve)68└── last_code.txt — Latest OAuth code captured (auto-generated)69```7071## Platform Setup Guides7273For step-by-step instructions on creating developer apps and getting credentials,74load `references/platform-setup-guides.md`. Each platform has:7576- Exact URLs to visit77- Buttons to click and fields to fill78- Where to find credentials79- How the auth flow works8081## Quick Reference: Auth Method Summary8283| Platform | Auth Method | What User Needs | Config Keys |84|----------|------------|----------------|-------------|85| **X** | OAuth 1.0a PIN | API Key + Secret | `x.api_key`, `x.api_secret` |86| **LinkedIn** | OAuth 2.0 | Client ID + Secret | `linkedin.client_id`, `linkedin.client_secret` |87| **Bluesky** | App password | Handle + App Password | `bluesky.handle`, `bluesky.app_password` |88| **Instagram** | OAuth 2.0 | App ID + Secret | `instagram.app_id`, `instagram.app_secret` |89| **Mastodon** | OAuth 2.0 | Instance + Client ID/Secret | `mastodon.instance`, `mastodon.client_id`, `mastodon.client_secret` |90| **Twitch** | OAuth 2.0 | Client ID + Secret | `twitch.client_id`, `twitch.client_secret` |91| **Reddit** | OAuth 2.0 | Client ID + Secret | `reddit.client_id`, `reddit.client_secret` |92| **Threads** | OAuth 2.0 | App ID + Secret | `threads.app_id`, `threads.app_secret` |93| **Facebook** | OAuth 2.0 | App ID + Secret | `facebook.app_id`, `facebook.app_secret` |94| **YouTube** | OAuth 2.0 | Client ID + Secret | `youtube.client_id`, `youtube.client_secret` |95| **Discord** | Webhook | Webhook URL | `discord.webhook_url` |96| **Slack** | Webhook | Webhook URL | `slack.webhook_url` |97| **Telegram** | Bot token | Token + Chat ID | `telegram.bot_token`, `telegram.chat_id` |98| **GitHub** | PAT | PAT + Repo | `github.pat`, `github.repo` |99100## Environment Variables101102All platforms support environment variable fallback when `config.json` is absent.103This is useful for CI/CD or ephemeral environments:104105| Env Var | Config Key |106|---------|-----------|107| `X_API_KEY` | `x.api_key` |108| `X_API_SECRET` | `x.api_secret` |109| `LINKEDIN_CLIENT_ID` | `linkedin.client_id` |110| `LINKEDIN_CLIENT_SECRET` | `linkedin.client_secret` |111| `INSTAGRAM_APP_ID` | `instagram.app_id` |112| `INSTAGRAM_APP_SECRET` | `instagram.app_secret` |113| `BLUESKY_HANDLE` | `bluesky.handle` |114| `BLUESKY_APP_PASSWORD` | `bluesky.app_password` |115| `MASTODON_INSTANCE` | `mastodon.instance` |116| `MASTODON_CLIENT_ID` | `mastodon.client_id` |117| `MASTODON_CLIENT_SECRET` | `mastodon.client_secret` |118| `TWITCH_CLIENT_ID` | `twitch.client_id` |119| `TWITCH_CLIENT_SECRET` | `twitch.client_secret` |120| `REDDIT_CLIENT_ID` | `reddit.client_id` |121| `REDDIT_CLIENT_SECRET` | `reddit.client_secret` |122| `THREADS_APP_ID` | `threads.app_id` |123| `THREADS_APP_SECRET` | `threads.app_secret` |124| `FACEBOOK_APP_ID` | `facebook.app_id` |125| `FACEBOOK_APP_SECRET` | `facebook.app_secret` |126| `YOUTUBE_CLIENT_ID` | `youtube.client_id` |127| `YOUTUBE_CLIENT_SECRET` | `youtube.client_secret` |128| `DISCORD_WEBHOOK_URL` | `discord.webhook_url` |129| `SLACK_WEBHOOK_URL` | `slack.webhook_url` |130| `TELEGRAM_BOT_TOKEN` | `telegram.bot_token` |131| `TELEGRAM_CHAT_ID` | `telegram.chat_id` |132| `GITHUB_PAT` | `github.pat` |133| `GITHUB_REPO` | `github.repo` |134135## Cross-Machine OAuth (Reverse Proxy Required)136137When the agent is on a remote machine and the user is on a different machine138(e.g., their desktop "Kratos"), OAuth redirect URLs need to point somewhere139the user's browser can reach.140141**You need a reverse proxy** that makes a local port accessible via HTTPS at a142URL you can reach from your browser. Options include:143144- **Tailscale Serve** — HTTPS on your tailnet, no public internet needed145- **Cloudflare Tunnel** (`cloudflared`) — public or private tunnels146- **Nginx Proxy Manager** — if you have a domain pointing to your VPS147- **ngrok** — quick public URL for testing148- **Your VPS hostname** — if it already has HTTPS149150The URL does NOT need to be public on the internet — it just needs to be151reachable by YOUR browser.152153### Setup (one-time)154155```bash156# 1. Start the callback server (persistent, port 19876)157python3 ~/.social-poster/oauth-callback-server.py 19876 &158159# 2. Use your reverse proxy to route paths to 127.0.0.1:19876160# Example with Tailscale: tailscale serve --bg --set-path /oauth-callback 19876161# Example with Cloudflare: cloudflared tunnel --url http://localhost:19876162# Example with nginx: proxy_pass http://127.0.0.1:19876 in your site config163164# The paths you need depend on which platforms you use:165# - /oauth-callback # For Mastodon, Twitch, Reddit, etc.166# - /integrations/social/linkedin # If using LinkedIn167# - /integrations/social/instagram # If using Instagram168```169170### How the Callback Server Works171172The Python HTTP server (`oauth-callback-server.py`) listens on `127.0.0.1:19876`.173Your reverse proxy forwards HTTPS requests to it. When an OAuth redirect arrives174with `?code=...`, the server saves the code to `{{SOCIAL_POSTER_DIR}}/last_code.txt`.175176The agent then reads the code and exchanges it for tokens. The server stays177alive between sessions and handles any number of callbacks.178179## Platform Character Limits180181When posting, respect per-platform limits:182183| Platform | Max Length | Notes |184|----------|-----------|-------|185| **X/Twitter** (non-premium) | 280 chars | Requires `who_can_reply_post: everyone` |186| **X/Twitter** (premium) | 25,000 chars | Same |187| **Bluesky** | 300 chars | Returns 400 if exceeded |188| **LinkedIn** | 3,000 chars | Professional tone |189| **Facebook** | 63,206 chars | Link for best reach |190| **Instagram** | 2,200 chars | Image/video required |191| **Threads** | 500 chars | Short and punchy |192| **YouTube** | 5,000 chars | Description only |193| **Mastodon** | 500 chars | Instance-dependent |194| **Twitch** | N/A | Channel announcements |195| **Reddit** | 40,000 chars | Title max 300 |196| **Discord** | 2,000 chars | Embeds supported |197| **Slack** | 40,000 chars | Block kit |198| **Telegram** | 4,096 chars | HTML/Markdown |199| **GitHub** | N/A | Issues/gists |200201## Pitfalls202203- **Never use `os.path.expanduser("~")` directly in Hermes sessions** — it resolves to the sandbox home. Scripts auto-detect and use `pwd.getpwuid()`.204- **config.json and vault.json must be chmod 600** — NEVER committed to git.205- **LinkedIn scopes must NOT include org scopes** unless the app has Community Management API.206- **LinkedIn MUST NOT use `prompt=none`** for first-time authorization.207- **X/Twitter callback must be `oob`** for PIN-based flow.208- **Instagram app MUST be Live** (not Development) for OAuth.209- **Bluesky uses app passwords** (format: `xxxx-xxxx-xxxx-xxxx`), not regular passwords.210- **Redirect URI MUST match exactly** between auth URL and token exchange POST.211- **`state` parameter is REQUIRED for all OAuth 2.0 flows** — built into all generators.212- **`post.py` is text-only** — any post carrying a picture needs `scripts/post_with_image.py` (same vault/config, per-platform copy from `<dir>/<platform>.md`). Verified upload recipes for X, LinkedIn, Bluesky, Mastodon and Discord are in `references/image-posts.md`.213- **LinkedIn `/rest/posts` returns 500 for every active version on this app** (202601-202608, 2026-09-15); post images via `/v2/assets?action=registerUpload` + `/v2/ugcPosts` with `shareMediaCategory: IMAGE`. A `urn:li:image:` URN from `/rest/images` in a ugcPost fails as "not owned by the author".214- **X returns 402 `credits depleted` regardless of code correctness** once the Pay-Per-Use balance hits zero. Auth and media upload still succeed, so diagnose in that order before touching the code.215- **Platform setup guides may drift** as developer portals update their UIs. If a user reports a step doesn't match, verify at the official developer portal URL and update the guide.216217## References218219- `references/platform-setup-guides.md` — Step-by-step setup guides for all 15 platforms220- `references/platform-expansion.md` — Full roadmap and implementation notes221- `references/oauth-flows.md` — OAuth implementation details and token exchange reference