Social Posting
Richfolio can optionally publish its buy signals to public social accounts — X/Twitter, a Facebook Page, a Threads account, and a LinkedIn Page — alongside the email + Telegram briefs. It runs only in daily and intraday modes (never weekly or refresh).
This is fully opt-in. If you set no social credentials, nothing is posted and the rest of Richfolio works exactly as before — each platform logs a “credentials not set — skipping” line and moves on.
What gets posted
Posts are deliberately generic so nothing private leaks:
- Only STRONG BUY and BUY signals (HOLD/WAIT are skipped).
- Each signal shows ticker, action, confidence, a short reason, and optionally a value rating — nothing else.
- Target-portfolio and watch-list tickers are merged uniformly as “signals” — there is no “portfolio vs watchlist” label, so your holdings are never revealed.
- Never posted: allocations, gaps, suggested buy amounts, share counts, total portfolio value.
buildSignalLines()insrc/socialContent.tsis the single allowlist chokepoint, andsanitizeReason()strips any allocation gap, dollar sizing, or overlap-discount text the AI may write into its reason. Both are covered by unit tests. - The ticker is rendered as a clickable
#hashtagon Facebook / Threads / LinkedIn (cashtags only work on X, where it stays a$cashtag). A configurable set of generic hashtags is appended on the non-X platforms to boost reach. - Every post ends with a disclaimer: “Not financial advice. Auto-generated by Richfolio.”
Enable / disable
A social block in config.json is the master switch (defaults shown):
"social": {
"enabled": true,
"includeLinkInX": false,
"hashtags": ["investing", "stocks", "stockmarket", "ETFs"]
}
enabled: false— disables all social posting regardless of credentials.includeLinkInX— include the analysis link in X posts. Off by default because a link raises X’s pay-per-use cost.hashtags— generic hashtags appended on Facebook / Threads / LinkedIn (the leading#is optional). Not added on X.
Each platform additionally gates on its own credentials, so a platform with no keys is skipped even when enabled is true.
Secrets summary
Add these as repository Secrets (Settings → Secrets and variables → Actions → Secrets). All are optional — set only the platforms you want.
| Secret | Platform | Notes |
|---|---|---|
FACEBOOK_PAGE_ID | Your Page’s numeric id | |
FACEBOOK_PAGE_TOKEN | Long-lived Page access token | |
THREADS_USER_ID | Threads | Your Threads numeric user id |
THREADS_ACCESS_TOKEN | Threads | Long-lived token (expires ~60 days) |
THREADS_TOKEN_PAT | Threads | Optional PAT to auto-refresh the Threads token (see below) |
LINKEDIN_ACCESS_TOKEN | OAuth 2.0 token with w_organization_social | |
LINKEDIN_ORG_URN | e.g. urn:li:organization:123456 | |
X_API_KEY / X_API_SECRET | X/Twitter | OAuth 1.0a consumer keys |
X_ACCESS_TOKEN / X_ACCESS_TOKEN_SECRET | X/Twitter | OAuth 1.0a user tokens |
Facebook Page setup
The runtime needs just two values: FACEBOOK_PAGE_ID and a long-lived FACEBOOK_PAGE_TOKEN. Because you post to your own Page as its admin, you can stay in the app’s Development mode — no App Review required.
1. Create a Meta app
- Go to developers.facebook.com → My Apps → Create App.
- Pick the Other → Business app type and name it (e.g. “Richfolio Poster”).
2. Add the Page-management use case
- In the app, open Use cases → choose Manage everything on your Page.
- Open its Permissions and Add (Standard Access is enough — ignore “Get Advanced Access”):
pages_manage_posts— publish to the feed (the essential one)pages_read_engagementpages_show_list
They should show “Ready for testing” — that means usable now for you as admin.
3. Get a user token
- Open the Graph API Explorer.
- Select your app → Get User Access Token → tick the three
pages_*permissions → Generate Access Token → approve.
4. Mint a long-lived Page token
Richfolio ships a helper that does the token exchange for you. Temporarily add to .env:
FACEBOOK_PAGE_ID=your_page_id # numeric Page id
FACEBOOK_APP_SECRET=... # App settings → Basic → App secret
FB_USER_TOKEN=... # the user token from step 3
Then run:
npx tsx smoke/fb-page-token.ts
It exchanges the short-lived user token for a long-lived one, finds your Page, and prints a non-expiring Page token. Paste that into FACEBOOK_PAGE_TOKEN, then delete FACEBOOK_APP_SECRET and FB_USER_TOKEN from .env — they are setup-only and never used at runtime.
5. Verify
npx tsx smoke/smoke-facebook.ts # checks the token (no posting)
npx tsx smoke/smoke-facebook.ts --post --cleanup # posts a test, then deletes it
A PASS confirms the token is a valid Page token and posting works end-to-end.
6. Add to GitHub
Add FACEBOOK_PAGE_ID and FACEBOOK_PAGE_TOKEN as repository Secrets. Do not add the app secret or user token.
Token longevity: the Page token is effectively permanent, but it dies if you change your Facebook password, revoke the app, or stop being a Page admin. If posting stops, re-mint from step 3.
Threads setup
Threads reuses the same Meta app as Facebook. You need a THREADS_USER_ID and a long-lived THREADS_ACCESS_TOKEN. The account you authorize is the one posts appear from — use your brand Threads account, which must be public.
1. Add the Threads use case to the Meta app: Use cases → Access the Threads API. Enable the scopes threads_basic and threads_content_publish.
2. Add yourself as a Threads Tester: App Dashboard → App roles → Roles → Add People → Threads Tester → enter your Threads username → accept the invite in the Threads app (Settings → Account → Website permissions).
3. Generate a long-lived token: in the Threads use-case Settings, the User Token Generator lists your tester account → Generate token. This is already long-lived — no exchange needed. Paste it into .env as THREADS_ACCESS_TOKEN.
4. Discover your user id and verify:
npx tsx smoke/smoke-threads.ts # prints THREADS_USER_ID; set it in .env
npx tsx smoke/smoke-threads.ts --post --cleanup # posts a test (delete may be manual)
5. Add THREADS_USER_ID and THREADS_ACCESS_TOKEN to GitHub Secrets.
Token expiry & auto-refresh: Threads long-lived tokens expire in ~60 days. The workflow
.github/workflows/refresh-threads-token.ymlrefreshes the token monthly and writes it back to the secret — if you also add aTHREADS_TOKEN_PATsecret (a fine-grained PAT with repository Secrets: Read and write). Without that PAT, refresh the token manually before it expires.
LinkedIn Page setup
LinkedIn is free but the most gated. You need:
- A LinkedIn Developer app associated with your Company Page.
- The “Community Management API” product, which grants the
w_organization_socialscope. Requesting it needs your app’s company association to be verified (Settings tab) and submission of an access form (it asks for a registered business name). - An OAuth 2.0 access token with
w_organization_social, generated by an admin of the Page.
Set:
LINKEDIN_ACCESS_TOKEN— the OAuth 2.0 token.LINKEDIN_ORG_URN— your organization URN, e.g.urn:li:organization:123456(the number is in your Company Page URL).LINKEDIN_API_VERSION— optional override (defaults to a recentYYYYMM).
Verify with npx tsx smoke/smoke-linkedin.ts (token check) and --post --cleanup (publish test).
LinkedIn access tokens expire in ~60 days (refresh tokens last ~365). Plan to refresh periodically. Posting to an organization requires a registered business; if you don’t have one, you can adapt the poster to post from a personal profile via the self-serve “Share on LinkedIn” product (
w_member_social).
X / Twitter setup
X has no free posting tier since February 2026. Posting is pay-per-use (~$0.015 per post, more if it contains a link). Richfolio ships the X poster but it stays dormant until you add keys.
Posting uses OAuth 1.0a user context. In the X Developer Portal, create a project/app with Read and Write permissions and generate:
X_API_KEY/X_API_SECRET— the consumer (API) key & secret.X_ACCESS_TOKEN/X_ACCESS_TOKEN_SECRET— the access token & secret for your account.
Leave includeLinkInX off in config.json to keep per-post cost low.
How it runs
In daily and intraday modes, after the email and Telegram sends, Richfolio calls sendSocialPosts() (src/social.ts). Each platform posts inside its own try/catch, so one platform failing never blocks the others — or the email/Telegram that already went out.
If you fork Richfolio, social posting stays off until you complete the setup above with your own accounts — credentials cannot be shared, since each token posts to its owner’s account.