social-agent turns selected Reddit posts into platform-native social drafts, banks reusable angles, fills four daily queue slots, and publishes to whichever platforms are enabled.
Current platform surface:
- Threads
- X/Twitter
- Facebook Group
Dashboard: http://localhost:4001
Maintainer context for humans and coding agents lives in AGENTS.md.
Production runtime notes live in docs/PRODUCTION_RUNTIME.md; Instagram image
persistence details live in docs/INSTAGRAM_IMAGE_PIPELINE.md.
- The runtime is now TS-first with a split model:
tsxfor source-driven dev tooling and compileddist/output for the production service. - X is now a first-class platform in the content model, queue, history, API, dashboard, and publish flow.
- X is text-only in this runtime and posts through
POST /2/tweets. - X live-post smoke testing is confirmed working with OAuth 2.0 user-context credentials as of April 29, 2026.
- The content engine banks source summaries plus reusable angles so one Reddit source can support multiple future posts.
Tracked default profile:
ENABLE_THREADS=trueENABLE_INSTAGRAM=trueENABLE_LINKEDIN=falseENABLE_X=falseENABLE_FACEBOOK=false
git clone https://github.com/OneClickPostFactory/social-agents.git
cd social-agent
npm install
cp .env.example .env
# fill in your credentials
npm run build
npm run fetch
npm run queue
npm run start:pm2
pm2 save && pm2 startup- Author in TypeScript.
- Use
npm run devfor source-driven local development throughtsx. - Use
npm run buildto compile the runtime intodist/. - Use
npm startornpm run start:pm2to run the compileddist/service in production. - Do not edit generated build output by hand.
- Runtime state now lives primarily in
data/automation.sqliteanddata/control-plane.sqlite. - The build copies
public/andcontent-os/intodist/so the compiled runtime stays self-contained.
Production intentionally runs compiled JavaScript from dist/, not TypeScript through tsx. TypeScript is the authoring format; JavaScript is the Node runtime artifact. This keeps production startup smaller, removes the TypeScript transpiler from the live service path, and proves the exact deployable artifact with npm run smoke:dist and npm run ci.
For the hosted OneClickPostFactory app, this repo also runs as a headless worker against an owner-managed Supabase project.
Flow:
Lovable app -> Supabase agent_jobs -> social-agent worker -> Supabase tenant tables
The browser app must not call this backend directly. The worker polls agent_jobs, processes each job by job.user_id, checks entitlement from Supabase profiles, decrypts tenant credentials from user_credentials, and writes tenant-scoped results back to queue_items, publish_history, source_records, angle_records, and worker_logs.
OpenClaw's social-agent-connector plugin calls this service over the
user-only data/social-connector.sock Unix socket. The registered tool is
social_connector; it exposes non-sensitive status, dry-run-gated
publish_post, provider verification, and a shared API/Browser Relay
idempotency ledger. The plugin does not load platform credentials and the tool
schema rejects credential-bearing arguments.
The connector truthfully leaves comments, replies, DMs, likes, follows, reposts, quote-posts, public discovery, and insights to Browser Relay because the current platform clients do not implement those API operations.
Start only the connector facade, without cron or queue automation:
npm run build
npm run start:connectorThe full agent also starts the same private connector socket as part of
npm start. Do not run both entrypoints against the same socket.
Required worker env:
SUPABASE_URL=https://<your-owned-project-ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<sb_secret_... or legacy service_role JWT>
CREDENTIAL_ENCRYPTION_KEY=<same-key-used-by-lovable-server-runtime>
OPENAI_IMAGE_MODEL=gpt-image-2
OPENAI_IMAGE_TIMEOUT_MS=120000
CLOUDINARY_CLOUD_NAME=<cloudinary-cloud-name>
CLOUDINARY_API_KEY=<cloudinary-api-key>
CLOUDINARY_API_SECRET=<cloudinary-api-secret>
CLOUDINARY_FOLDER=social-agent/instagramCloudflare production no longer uses Worker-side Reddit public JSON/RSS or
Browser Run web-login collection as the active source path. Reddit OAuth is not
available as a reliable product path for this project. Active Reddit collection
is the user-installed OneClick Reddit Connector: it pairs with the tenant's
OneClick account, reads only configured Reddit sources from the user's own
browser context, and writes tenant-scoped source_records before any later
OpenAI, angle, queue, or publishing work. Connector writes require an enabled
subreddit source and an enabled Reddit username source owned by the same user;
records with a mismatched subreddit or author fail closed. Manual import remains
an advanced fallback only. The tenant's Reddit source intent still comes from
user_sources; runtime env must not be used as a global tenant author/subreddit
fallback. See docs/ADR_REDDIT_INGESTION_DECISION.md before changing this
direction.
Run only the worker loop with:
npm run worker:supabaseProduction should run the compiled worker entrypoint:
npm run build
npm run start:supabaseFor Cloudflare production, deploy the scheduled Worker:
npm run deploy:cloudflareCloudflare runs src/cloudflare-worker.ts from a cron trigger every minute. Each scheduled tick claims a small batch of Supabase jobs, processes them, and exits. The local setInterval loop remains available for development through npm run worker:supabase.
Set Cloudinary credentials as Cloudflare Worker secrets. OPENAI_IMAGE_MODEL,
OPENAI_IMAGE_TIMEOUT_MS, and CLOUDINARY_FOLDER are non-secret Worker vars
in wrangler.toml. OPENAI_IMAGE_TIMEOUT_MS controls only OpenAI image
generation; do not remove image timeouts entirely.
Local SQLite remains for local admin/dev control-plane state. It is not the SaaS tenant source of truth.
Set:
ENABLE_LINKEDIN=true
LINKEDIN_TOKEN=...
LINKEDIN_PERSON_URN=urn:li:person:...
LINKEDIN_REFRESH_TOKEN=...
LINKEDIN_CLIENT_ID=...
LINKEDIN_CLIENT_SECRET=...
LINKEDIN_EXPIRES_AT=...The LinkedIn publisher uses the UGC Posts API and is text-only. When the refresh token and matching client credentials are present, the worker renews the access token within seven days of expiry (or on the first publish when expiry metadata is not known) and persists rotated tokens encrypted. LinkedIn programmatic refresh tokens are available only to approved Marketing Developer Platform partners; otherwise the account must reconnect through the normal authorization-code flow.
Set OAuth 2.0 user-context credentials:
ENABLE_X=true
X_OAUTH2_ACCESS_TOKEN=...
X_OAUTH2_REFRESH_TOKEN=...
X_CLIENT_ID=...
X_CLIENT_SECRET=...
X_REDIRECT_URI=http://127.0.0.1:4001/auth/x/callbackX_OAUTH2_CLIENT_ID and X_OAUTH2_CLIENT_SECRET are also accepted aliases for the labels shown in the X developer portal.
OAuth 1.0a user-context credentials are still supported:
ENABLE_X=true
X_API_KEY=...
X_API_SECRET=...
X_ACCESS_TOKEN=...
X_ACCESS_TOKEN_SECRET=...Notes:
- Preferred path is OAuth 2.0 user-context credentials with an access token, refresh token, client ID, and client secret from the same X app.
- OAuth 2.0 user-context posting is supported through the dashboard connect flow at
/auth/x/startor by importing portal-generated tokens withnpm run import-x-oauth2. - OAuth 1.0a user-context auth also posts through
POST /2/tweets. - App-only bearer tokens are not valid for posting.
- Validate the token with
npm run test-x. - Publish a deliberate live smoke test with
npm run test-x -- --live-post. - The latest confirmed live smoke test posted as
@JohnWOE15:https://x.com/i/web/status/2049570569494958455. - If auth passes but publish is rejected by X for credits or access tier, the app automatically keeps X in draft-only mode for a cooldown window so other platforms can continue publishing.
Threads and Instagram credentials are intentionally separate. Never replace a working Instagram/Page token with a Threads user token.
Create or open the Threads app in the
Meta App Dashboard. Store its
THREADS_APP_SECRET only as a server/Worker secret. A valid one-hour Threads
user token can then be exchanged server-side for a 60-day token, and an
unexpired long-lived token is refreshed before publishing. Expired tokens
cannot be exchanged or refreshed and must be authorized again.
Get the relevant user tokens from Meta Graph API Explorer with:
threads_basicthreads_content_publishinstagram_basicinstagram_content_publishpages_read_engagementpages_show_listpublish_to_groupsgroups_access_member_info
Resolve the Threads user ID without placing the token in the URL:
curl -H "Authorization: Bearer YOUR_THREADS_TOKEN" \
"https://graph.threads.net/me?fields=id,username"Resolve the Instagram account from the linked Page:
curl "https://graph.facebook.com/v25.0/me/accounts?access_token=YOUR_TOKEN"
curl "https://graph.facebook.com/v25.0/PAGE_ID?fields=instagram_business_account&access_token=YOUR_TOKEN"If FACEBOOK_PAGE_ID is set, the app can auto-discover the linked Instagram business account and derive a Page access token for Instagram publishing.
Runtime boundary: Threads and Instagram publication is retired in this project. The local adapters retain read-only identity/verification support but fail closed before any Meta write. Approved Threads and Instagram work must enter the canonical workspace outbox workers.
Find the Facebook Group ID from facebook.com/groups/GROUP_ID.
| Platform | Format | Writing shape | Media |
|---|---|---|---|
| Text post | Concrete, professional, operational | None | |
| Threads | Text post | Punchy, direct, conversational | None |
| X | Text post | Sharp, reply-worthy, under 280 chars | None |
| Caption + image | Visual, clear, save-worthy | GPT Image asset persisted to Cloudinary before queue/publish | |
| Text post | Conversational with practical framing | None |
package.json is strict JSON, so the scripts themselves cannot have real comments. This table is the script commentary and should be updated whenever a script is added or renamed.
| Command | Why it exists | What it runs |
|---|---|---|
npm run typecheck |
Catch TypeScript contract errors before a build or deploy. | tsc --noEmit --project tsconfig.json |
npm run build |
Produce the deployable runtime artifact under dist/. |
tsx scripts/build.ts |
npm run test |
Run the focused local security regression suite against compiled output. | npm run build && node dist/test/security-hardening.test.js |
npm run smoke:dist |
Prove the compiled CLI can boot and read runtime state. | node dist/src/cli.js status |
npm run ci |
Run the full local release gate in one command. | typecheck, build, dist smoke, security tests |
npm run dev |
Start the scheduler and dashboard directly from TypeScript during local development. | tsx src/agent.ts |
npm run worker:supabase |
Run only the hosted SaaS worker loop against Supabase jobs. | tsx src/supabase-worker.ts |
npm start |
Start the production service from compiled JavaScript. | node dist/src/agent.js |
npm run start:supabase |
Start the production SaaS worker from compiled JavaScript. | node dist/src/supabase-worker.js |
npm run start:pm2 |
Start the compiled production service under PM2 supervision. | pm2 start dist/src/agent.js --name social-agent --restart-delay=5000 |
npm run connector |
Start only the private OpenClaw social connector from TypeScript; no cron or queue worker. | tsx src/social-connector-main.ts |
npm run start:connector |
Start only the compiled private OpenClaw social connector; no cron or queue worker. | node dist/src/social-connector-main.js |
npm run deploy:cloudflare |
Deploy the SaaS worker as a Cloudflare scheduled Worker. | wrangler deploy |
npm run deploy -- --ref origin/main |
Backup data, rebuild, restart PM2, and health-check a deployment. | tsx scripts/deploy.ts |
npm run backup |
Snapshot runtime state before deploys or risky operations. | tsx scripts/backup.ts |
npm run restore -- --from <backup-dir> |
Restore runtime state from a previous backup. | tsx scripts/restore.ts |
npm run fetch |
Fill empty queue slots from banked angles or fresh Reddit sources. | tsx src/cli.ts fetch |
npm run queue |
Inspect queued drafts, publish IDs, and retry errors. | tsx src/cli.ts queue |
npm run status |
Check slot occupancy and memory counts quickly. | tsx src/cli.ts status |
npm run memory |
Inspect source and angle inventory. | tsx src/cli.ts memory |
npm run history |
Review recent publish history. | tsx src/cli.ts history |
npm run post-now |
Publish queued slots immediately through the same automation gate as cron/API. | tsx src/cli.ts post-now |
npm run import-x-oauth2 |
Import X OAuth 2.0 user-context tokens generated in the X developer portal. | tsx src/cli.ts import-x-oauth2 |
npm run test-meta |
Diagnose Meta credentials, Pages, Instagram linkage, Threads, and Group access. | tsx src/test-meta.ts |
npm run test-x |
Validate the configured X auth mode and optionally live-post a smoke test. | tsx src/test-x.ts |
- Draft generation skips disabled platforms to protect token spend.
- Existing queued items can auto-hydrate missing X drafts from stored source and angle memory when X is enabled later.
- X is confirmed working with the current OAuth 2.0 user-context app; if X later rejects publishing for credits or access tier, the runtime falls back to draft-only mode for X.
- Instagram image generation only runs when Instagram is enabled, and generated images are uploaded to Cloudinary immediately so queued posts do not rely on temporary OpenAI provider URLs.
- Failed platforms no longer force the whole queue item to disappear.
- Partial success is supported, so one platform can succeed while another is retained for retry.
- The API, CLI, and cron now share the same automation gate and SQLite-backed lock layer.