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.com → My Apps → Create App.
  2. Pick the Other → Business 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 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.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. Since it is set but unrefreshable, the workflow now fails loudly in that state rather than passing silently — which is how three green monthly runs once hid an expired token.


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.