NewDayKnowledge

How this AI news platform was built, and how it works

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.

  1. Add LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET to .env.
  2. In the LinkedIn app's settings, allow http://localhost:8765/callback as an OAuth redirect URL.
  3. Run: npx tsx scripts/linkedin-auth.ts
  4. Copy the printed LINKEDIN_ACCESS_TOKEN and LINKEDIN_PERSON_URN into .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.

  1. Sign up at zernio.com (free, first 2 connected accounts, no card required).
  2. Connect your LinkedIn Company Page from Zernio's dashboard.
  3. Generate an API key in Zernio's settings.
  4. Add ZERNIO_API_KEY and ZERNIO_LINKEDIN_ACCOUNT_ID to .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.
  5. 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.

  1. Generate a secret: openssl rand -hex 24
  2. Add that value as AUTO_PUBLISH_SECRET in both your local .env and Vercel's Environment Variables.
  3. 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).
  4. Create a scheduled job pinging this URL every 5 minutes: https://YOUR-DEPLOYED-URL/api/cron/auto-publish?secret=YOUR_SECRET
  5. 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.
  6. 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.

  1. Generate a new secret just for analytics: openssl rand -hex 24
  2. Add that value as SYNC_ANALYTICS_SECRET in both your local .env and Vercel's Environment Variables. Do not reuse AUTO_PUBLISH_SECRET.
  3. In cron-job.org, or the equivalent HTTP scheduler used for auto-publish, create a separate job that runs once a day.
  4. Point the daily job at: https://YOUR-DEPLOYED-URL/api/cron/sync-analytics?secret=YOUR_SECRET
  5. 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.

  1. Generate a new secret just for the health check: openssl rand -hex 24
  2. Add that value as HEALTH_CHECK_SECRET in both your local .env and Vercel's Environment Variables. Do not reuse AUTO_PUBLISH_SECRET or SYNC_ANALYTICS_SECRET.
  3. 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.
  4. Point the job at: https://YOUR-DEPLOYED-URL/api/cron/health-check?secret=YOUR_SECRET
  5. Make sure TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID are 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.

  1. In Telegram, message @BotFather and send /newbot.
  2. Copy the bot token into TELEGRAM_BOT_TOKEN.
  3. Message your new bot once from the Telegram account that should receive alerts.
  4. Open https://api.telegram.org/bot<TOKEN>/getUpdates, replacing <TOKEN> with your real bot token, and copy your chat id into TELEGRAM_CHAT_ID.
  5. Add TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID in Vercel's Environment Variables and in your local .env if 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.

  1. Generate another random secret: openssl rand -hex 24
  2. Add it as TELEGRAM_WEBHOOK_SECRET in Vercel's Environment Variables. Keep TELEGRAM_BOT_TOKEN and the admin's exact TELEGRAM_CHAT_ID configured too.
  3. Deploy the app so the webhook has a real public HTTPS URL.
  4. After deployment, manually register the webhook once with Telegram's setWebhook API. Set url to https://YOUR-DEPLOYED-URL/api/telegram/webhook and secret_token to the exact TELEGRAM_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

  1. Clone the repository and run npm install.
  2. Copy .env.example to .env and fill in the values described in each section above, plus DATABASE_URL (a Postgres connection string -- this project uses Neon, a free hosted Postgres) and NEXTAUTH_SECRET (any random string).
  3. Run npx prisma migrate deploy to apply the database schema.
  4. Run npm run dev to start the dashboard locally.
  5. Run npm run agent:news to 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.