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() in src/socialContent.ts is the single allowlist chokepoint, and sanitizeReason() 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 #hashtag on 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 Facebook Your Page’s numeric id
FACEBOOK_PAGE_TOKEN Facebook 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 LinkedIn OAuth 2.0 token with w_organization_social
LINKEDIN_ORG_URN LinkedIn 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

  1. Go to developers.facebook.comMy AppsCreate App.
  2. Pick the OtherBusiness app type and name it (e.g. “Richfolio Poster”).

2. Add the Page-management use case

  1. In the app, open Use cases → choose Manage everything on your Page.
  2. 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_engagement
    • pages_show_list

    They should show “Ready for testing” — that means usable now for you as admin.

3. Get a user token

  1. Open the Graph API Explorer.
  2. 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 casesAccess the Threads API. Enable the scopes threads_basic and threads_content_publish.

2. Add yourself as a Threads Tester: App Dashboard → App roles → RolesAdd PeopleThreads 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.yml refreshes the token monthly and writes it back to the secret — if you also add a THREADS_TOKEN_PAT secret (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:

  1. A LinkedIn Developer app associated with your Company Page.
  2. The “Community Management API” product, which grants the w_organization_social scope. 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).
  3. 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 recent YYYYMM).

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.


Richfolio — free, open-source portfolio monitoring. · Privacy Policy

This site uses Just the Docs, a documentation theme for Jekyll.