🔮 Bold Little Oracle ▸ vega-overnight/POSTING-INFRA.md
updated 2026-04-16

Posting Infrastructure — Auto-Posting Architecture

Purpose: Move BLO’s social presence from “manually posted via Meta Business Suite browser” to a queued, scheduled, multi-channel pipeline. Recommend ONE architecture. Author: Vega (with technical hand-offs where needed)


1. Instagram — Meta Graph API (the one that’s currently broken)

Current state (per memory, 12d old — verify)

What Joshua needs to click (in order) to enable API posting

The issue per memory is “permissions not selectable in current app type.” Almost certainly: the app is configured as the wrong type, OR the Instagram Platform product is not added, OR Page Publishing Authorization (PPA) hasn’t been completed on the FB Page itself.

Path A — Preferred: “Instagram API with Facebook Login”

In order, at https://developers.facebook.com/apps/3852884318351764/ :

  1. Dashboard → App Settings → Basic - Confirm App Type is Business (not Consumer, not None). If not, change it. This is the gate that makes Instagram Graph permissions selectable. - App Domains must include: boldlittleoracle.unicorn.land - Privacy Policy URL: https://boldlittleoracle.unicorn.land/privacy - Terms of Service URL: https://boldlittleoracle.unicorn.land/support (or /terms) - Category: Lifestyle or Business and Pages

  2. Left sidebar → “Add Product” - Find “Instagram” → Click “Set Up” - Choose: “Instagram API with Facebook Login” (NOT “Instagram API with Instagram Login” — that’s a different product flow)

  3. Instagram → API Setup with Facebook Login - Add the Instagram Business account (@boldlittleoracle). - Confirm it’s linked to FB Page 1022988797569167.

  4. Add Product → Facebook Login for Business - Configuration: create a new “Facebook Login for Business” configuration - Variation: “Instagram Business Login via Facebook” (this is the one memory flagged as missing) - Permissions to request (toggle on):

    • instagram_basic
    • instagram_content_publish
    • pages_read_engagement
    • pages_show_list
    • business_management (optional but recommended for multi-asset access)
    • Valid OAuth Redirect URIs: https://boldlittleoracle.unicorn.land/api/meta/callback (add to _worker.js routes)
  5. App Review → Permissions and Features - Request Advanced Access for: instagram_content_publish, instagram_basic, pages_read_engagement - Standard Access may be enough for a single-account tool, but Advanced is safer. - Review requires: screencast of OAuth flow + use case description. Template: > “This app is used by Bold Little Oracle (a solo developer) to programmatically publish daily oracle card posts to our own @boldlittleoracle Instagram Business account connected to Facebook Page 1022988797569167. No third-party user content is posted. All posts are authored by the page owner.”

  6. Facebook Page → Page Publishing Authorization (PPA) - Go to FB Page 1022988797569167 settings - Security → Page Publishing Authorization - Complete ID verification if prompted. - Note from Meta docs: “An Instagram professional account connected to a Page that requires PPA cannot be published to until PPA has been completed.” This is often the silent blocker.

  7. Generate a long-lived Page Access Token - Graph API Explorer → select app → “Get Page Access Token” → select FB Page → grant permissions listed above. - Exchange short-lived (1h) → long-lived (60d) via: GET /oauth/access_token? grant_type=fb_exchange_token& client_id={app-id}& client_secret={app-secret}& fb_exchange_token={short-lived-token} - Store the long-lived token in a Cloudflare Worker secret (not in code, not in git).

  8. Test publish via cURL ```bash # Step 1: Create media container curl -X POST “https://graph.facebook.com/v21.0/{ig-user-id}/media” \ -d “image_url=https://boldlittleoracle.unicorn.land/posts/test.jpg” \ -d “caption=Test from API” \ -d “access_token={long-lived-token}”

# Step 2: Publish curl -X POST “https://graph.facebook.com/v21.0/{ig-user-id}/media_publish” \ -d “creation_id={container-id-from-step-1}” \ -d “access_token={long-lived-token}” ```

Rate limit: 100 API-published posts per 24h rolling window. Far more than needed.

Common failure modes (if step 7 fails)


2. Bluesky — AT Protocol API

Auth model: App Password (simpler than OAuth). Bluesky handle + app-password → JWT.

Setup: 1. bsky.app → Settings → Privacy and Security → App Passwords → Create new 2. Label it “BLO-poster”. Save the password — shown once. 3. Store as BSKY_APP_PASSWORD secret.

Post flow (pseudo):

POST https://bsky.social/xrpc/com.atproto.server.createSession
  { identifier: "boldlittleoracle.bsky.social", password: APP_PASSWORD }
  → returns { accessJwt, refreshJwt, did }

POST https://bsky.social/xrpc/com.atproto.repo.createRecord
  Authorization: Bearer {accessJwt}
  {
    repo: "{did}",
    collection: "app.bsky.feed.post",
    record: { text: "...", createdAt: ISO8601, langs: ["en"] }
  }

For images: com.atproto.repo.uploadBlob first → include blob ref in post record embed.

Rate limits (2026): 5,000 points/hour, 35,000 points/day. Create = 3 points. Means 1,666 posts/hour worst case. A non-issue for BLO.

Library: @atproto/api npm package (TypeScript). Works in Cloudflare Workers without polyfills.


3. Reddit — Reddit API (OAuth)

Auth model: OAuth2 with a “script” app type (since u/obadiah_finch is a single user).

Setup: 1. reddit.com/prefs/apps → create app → type script 2. Record: client_id, client_secret, username, password. 3. Store all as Worker secrets.

Auth flow:

POST https://www.reddit.com/api/v1/access_token
  Authorization: Basic base64(client_id:client_secret)
  Content-Type: application/x-www-form-urlencoded
  grant_type=password&username=obadiah_finch&password={pw}
  User-Agent: BoldLittleOracle/1.0 (by u/obadiah_finch)
  → { access_token, expires_in: 3600 }

POST https://oauth.reddit.com/api/submit
  Authorization: Bearer {access_token}
  User-Agent: BoldLittleOracle/1.0
  sr={subreddit}&kind=self&title={title}&text={body}

Rate limits: 60 requests/min authenticated. Plenty.

WARNING: Reddit detests automated posting of promotional content. Use this ONLY for: - Scheduling already-human-drafted value posts (which are approved in PUSH-QUEUE.md) - Automation for Obadiah’s own account should NEVER run more than 1 post/day across all subs - Comments should remain 100% manual. Reddit’s anti-spam ML is aggressive.

Library: direct fetch is simpler than snoowrap in a Worker. 60 LOC total.


4. TikTok — Content Posting API

Auth model: OAuth2 with video.publish scope.

Setup: 1. developers.tiktok.com → register as developer 2. Create app, connect TikTok account (@boldlittleoracle — create if not exists) 3. Apply for video.publish scope via Audit process.

Approval blocker: Apps in unaudited status can only post in “private viewing mode” — i.e., the post exists but no one can see it. Useless.

Audit requirements (2026): - 5-10 business day review (per web research, Apr 2026) - Requires: working demo, clear use case doc, UX compliance (creator info retrieval before publish, consent screen) - Caps: ~15 posts per creator account per 24h once approved

Verdict: TikTok API is NOT worth building for BLO in week 1. The audit cycle + UX compliance for a single-account self-post tool is weeks of overhead for zero gain. Recommendation: post TikTok manually through the mobile app for the first 30-60 days. Reconsider API once posting frequency exceeds 2/day AND manual becomes the bottleneck.


5. Scheduler Architecture — RECOMMENDATION

Options evaluated

Option Pros Cons
A. Cloudflare Worker + Cron Trigger Infra already there (bold-little-oracle Pages project + _worker.js handles /api/subscribe). Free tier allows cron. No new machines. Secrets live in CF dashboard. No rich queue UI. Logs via wrangler tail.
B. Local cron on Joshua’s machine Dead simple. crontab -e + a Node script. Requires machine on. Not resilient. Secrets on disk.
C. Cambium Rhythms Joshua’s own agent framework — dogfooding. Native scheduling. Adds a dependency on an in-development system. Overkill for 4 channels.
D. Buffer / Hootsuite / Postiz GUI, done for you. Paid, generic, can’t do Reddit or Bluesky well, defeats “built by Obadiah” narrative.

Recommendation: Option A — Cloudflare Worker with Cron Trigger

Reasons: 1. Infrastructure already deployed (_worker.js on Pages project bold-little-oracle, account bc0c90d4bc8c0d8b4b247f42c57aa65b). 2. Secrets management already in use (Beehiiv key is already stored server-side per memory). 3. Cron triggers are free and run every N minutes. 4. Posts can be drafted as JSON files in a /posts directory in the repo — git-reviewable, deterministic, versioned. 5. No new infra for Joshua to own/pay for. 6. Cambium Rhythms (Option C) is a great future home but not worth blocking on.

Minimal architecture

bold-little-oracle/
├── posts/
│   ├── 2026-04-17-ig-day1.json   # { platform, publishAt, body, imageUrl, hashtags }
│   ├── 2026-04-17-bsky-day1.json
│   └── ...
├── website/
│   └── _worker.js                 # existing Beehiiv proxy
└── workers/
    └── scheduler/
        ├── wrangler.toml          # cron = "*/15 * * * *"  (every 15 min)
        ├── src/index.ts           # fetches pending posts, publishes, marks done
        └── src/providers/
            ├── instagram.ts       # Graph API Page post
            ├── bluesky.ts         # AT Proto
            └── reddit.ts          # OAuth script

Post JSON shape:

{
  "id": "2026-04-17-ig-day1",
  "platform": "instagram",
  "publishAt": "2026-04-17T13:00:00Z",
  "status": "queued",
  "body": "I'm Obadiah. I built an oracle card app...",
  "imageUrl": "https://boldlittleoracle.unicorn.land/posts/day1-hero.jpg",
  "hashtags": ["#oraclecards", "#witchtok", "#dailyoracle", "#indieapp", "#oracledeck"]
}

State: Store status and publishedAt in Cloudflare KV or a R2 JSON file. Never mutate the post .json in git (keeps history clean).

Failure handling: If publish errors, mark status: "error", store error message, alert Joshua via Beehiiv webhook → email, or push to a Discord webhook.

Manual approval toggle: Add status: "pending-approval" state. Joshua hits a URL (/ops/approve?id=...) to flip to queued. Default new posts to pending-approval for first 2 weeks.

Build effort

Total: ~1 day of engineering once Meta config is unblocked.

Why NOT Cambium Rhythms

Cambium is early. BLO revenue pressure is immediate. Ship the thinnest thing. Port to Rhythms later — the post JSONs are portable, the providers are just fetch calls.


6. What to Build FIRST (sequence)

  1. Today (Joshua, 30 min): Complete Meta App config steps 1-6 above. This unblocks everything else.
  2. Tomorrow (Joshua or agent, 1h): Create u/obadiah_finch on Reddit. Create Bluesky handle (@boldlittleoracle.bsky.social). Create app passwords / OAuth creds. Store in CF Worker secrets.
  3. Day 3 (1 day of eng): Ship scheduler Worker MVP with IG + Bluesky providers only. Reddit stays manual. TikTok stays manual.
  4. Day 4: Convert PUSH-QUEUE.md entries to post .json files, test publish one to IG from the queue.
  5. Day 5+: Publish daily per CONTENT-CALENDAR.

7. Observability


8. Risk register

Risk Likelihood Mitigation
Meta config still fails after steps Medium Fall back: keep posting via Meta Business Suite manually until App Review clears
Bluesky app password revoked by bsky breach Low Rotate quarterly; auto-refresh is not needed at this scale
Reddit auto-flags u/obadiah_finch as spam Medium Manual posting for first 30 days; API only for scheduled VALUE posts (not promo)
Joshua forgets to approve queued posts High Build slack/discord ping with one-click approve URL
Cloudflare Worker cold-start misses a cron tick Low Cron triggers are reliable; backfill logic scans “should have published by now” every 15 min