Setup Guides
Every practical, step-by-step setup this project needs, in one place. For why each of these exists, see architecture.md and tech-stack.md. For the milestone each one was introduced in, see build-guide.md.
LinkedIn personal profile posting
Requires a LinkedIn Developer app with the Share on LinkedIn product enabled.
- Add
LINKEDIN_CLIENT_IDandLINKEDIN_CLIENT_SECRETto.env. - In the LinkedIn app's settings, allow
http://localhost:8765/callbackas an OAuth redirect URL. - Run:
npx tsx scripts/linkedin-auth.ts - Copy the printed
LINKEDIN_ACCESS_TOKENandLINKEDIN_PERSON_URNinto.env.
LinkedIn access tokens expire after roughly 60 days; rerun the script when posting starts failing due to an expired token.
LinkedIn Company Page posting (via Zernio)
LinkedIn's own Company Page API needs a separate approval that has a hard structural requirement this project's existing app cannot meet (see troubleshooting.md, entry 13). Zernio, a free third-party service that already holds that approval, is used instead.
- Sign up at zernio.com (free, first 2 connected accounts, no card required).
- Connect your LinkedIn Company Page from Zernio's dashboard.
- Generate an API key in Zernio's settings.
- Add
ZERNIO_API_KEYandZERNIO_LINKEDIN_ACCOUNT_IDto.env. The account ID can be fetched by calling Zernio's accounts endpoint with the API key as a bearer token and reading the id field for the LinkedIn entry. - The Agent Config page's LinkedIn Company Page posting switch controls whether this is active; it defaults to on, and can be turned off instantly from the dashboard with no redeploy.
Twitter/X posting
Requires a developer app from developer.x.com with OAuth 1.0a credentials and Read and Write permissions. The X API charges per post (roughly $0.015 to $0.20 depending on whether the post includes a link), which is why this project keeps Twitter/X drafts hidden by default and has not enabled real posting to it.
Auto-publish
Auto-publish never publishes directly from the cron request. A ready-marked APPROVED LinkedIn post goes to the Telegram photo decision. If nothing is ready, the best eligible fallback first needs candidate approval and then the separate photo decision. A draft is never auto-approved.
- Generate a secret:
openssl rand -hex 24 - Add that value as
AUTO_PUBLISH_SECRETin both your local.envand Vercel's Environment Variables. - Sign up for a free account at cron-job.org, or an equivalent free HTTP scheduler (Vercel's own free Cron only fires once a day at an imprecise time, which is not accurate enough for a real countdown -- see troubleshooting.md, entry 12, for the related CI-specific issue this kind of platform limitation caused).
- Create a scheduled job pinging this URL every 5 minutes:
https://YOUR-DEPLOYED-URL/api/cron/auto-publish?secret=YOUR_SECRET - On the Agent Config page: enable auto-posting, add one or more daily times (shown and entered in your own local time, converted to UTC automatically), and choose whether auto-published LinkedIn posts use the Personal or Company destination.
- Per-post: open the Ready to Publish tab and turn on Include in auto-publish for any specific approved post you want to skip fallback candidate approval. Ready-marking never skips the Telegram photo decision.
LinkedIn engagement analytics sync
The analytics sync uses the existing ZERNIO_API_KEY to refresh real LinkedIn results for published posts from the last 30 days. Its scheduler secret is deliberately separate from the auto-publish secret, so either job can be revoked without affecting the other.
- Generate a new secret just for analytics:
openssl rand -hex 24 - Add that value as
SYNC_ANALYTICS_SECRETin both your local.envand Vercel's Environment Variables. Do not reuseAUTO_PUBLISH_SECRET. - In cron-job.org, or the equivalent HTTP scheduler used for auto-publish, create a separate job that runs once a day.
- Point the daily job at:
https://YOUR-DEPLOYED-URL/api/cron/sync-analytics?secret=YOUR_SECRET - After the first successful sync, open the Analytics dashboard to see real impressions, likes, comments, engagement rates, and post links.
News agent health check
The health check runs on the deployed Vercel app so it can warn you even when the Mac that runs the news agent is asleep or the local agent has silently stopped. It stays quiet while a run has started within the last 30 hours and sends at most one warning per 24 hours while a problem persists.
- Generate a new secret just for the health check:
openssl rand -hex 24 - Add that value as
HEALTH_CHECK_SECRETin both your local.envand Vercel's Environment Variables. Do not reuseAUTO_PUBLISH_SECRETorSYNC_ANALYTICS_SECRET. - In cron-job.org, or the equivalent external HTTP scheduler used for the other cron routes, create a separate job that runs every 3 hours.
- Point the job at:
https://YOUR-DEPLOYED-URL/api/cron/health-check?secret=YOUR_SECRET - Make sure
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDare also configured in Vercel so an unhealthy result can reach your phone.
Telegram notifications
Telegram notifications are optional and best-effort. If they are missing or failing, the dashboard still saves the in-app notification.
- In Telegram, message
@BotFatherand send/newbot. - Copy the bot token into
TELEGRAM_BOT_TOKEN. - Message your new bot once from the Telegram account that should receive alerts.
- Open
https://api.telegram.org/bot<TOKEN>/getUpdates, replacing<TOKEN>with your real bot token, and copy your chat id intoTELEGRAM_CHAT_ID. - Add
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDin Vercel's Environment Variables and in your local.envif you also want local agent digest messages.
Telegram auto-publish approvals
Every selected post pauses for a photo choice. Reply NO to publish text-only, or YES to generate and receive the real Layerre photo for final confirmation. At confirmation, PUBLISH/CONFIRM publishes with that exact photo; REJECT/NO retains it unapproved without publishing. A fallback has a separate first candidate YES/NO; candidate YES only advances to the photo question and never publishes by itself.
Only one candidate-approval, photo-decision, or photo-confirmation state is active globally. Confirmation has reply precedence, followed by photo decision and candidate approval. The slot's AutoPublishRun reservation remains in place throughout the Telegram round trip, so scheduler retries cannot refire it.
The app also fences the post as non-retryable before contacting the social provider. If the provider accepts it but saving the returned post ID fails, Telegram requests manual reconciliation with that external ID; the app never retries automatically, avoiding the risk of a duplicate post.
- Generate another random secret:
openssl rand -hex 24 - Add it as
TELEGRAM_WEBHOOK_SECRETin Vercel's Environment Variables. KeepTELEGRAM_BOT_TOKENand the admin's exactTELEGRAM_CHAT_IDconfigured too. - Deploy the app so the webhook has a real public HTTPS URL.
- After deployment, manually register the webhook once with Telegram's
setWebhookAPI. Seturltohttps://YOUR-DEPLOYED-URL/api/telegram/webhookandsecret_tokento the exactTELEGRAM_WEBHOOK_SECRET. For example:curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" --data-urlencode "url=https://YOUR-DEPLOYED-URL/api/telegram/webhook" --data-urlencode "secret_token=$TELEGRAM_WEBHOOK_SECRET"
Do this manual registration only after deploy; a local development URL cannot be registered because Telegram requires a reachable HTTPS endpoint. The route rejects deliveries without the webhook secret and silently ignores messages from every chat except the configured admin chat.
Local development
- Clone the repository and run
npm install. - Copy
.env.exampleto.envand fill in the values described in each section above, plusDATABASE_URL(a Postgres connection string -- this project uses Neon, a free hosted Postgres) andNEXTAUTH_SECRET(any random string). - Run
npx prisma migrate deployto apply the database schema. - Run
npm run devto start the dashboard locally. - Run
npm run agent:newsto run the news agent once by hand, or set up the launchd schedule described in the root README for daily automatic runs.
Running the test suite
npm run test:e2e runs the full Playwright end-to-end suite locally. It uses whatever DATABASE_URL is currently set, so it is safe to run against local development but deliberately never mutates real data (see the safety note in playwright.config.ts). The same suite also runs automatically in GitHub Actions on every push, against its own disposable database, never the real one.