Public
YouTube trend → AI clip → TikTok auto-post, every 12 hours
automationcrontelegramtiktokyoutube
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.

Trend Video AutoPoster

A scheduled Val Town job that runs every 12 hours (0 */12 * * *, UTC) and automates the whole short-form content loop: find a trending YouTube video, have AI cut it into a vertical clip, publish that clip straight to TikTok, and email you the result.

Rendering mermaid diagram...

Every step is wrapped in its own try/catch. A failure logs to the console, records the attempt, emails you, and returns cleanly — so the next scheduled run still fires.

Files

FileRole
main.tsThe interval handler + pipeline orchestration
lib/config.tsTunables: MAX_ATTEMPTS, AUTH_ALERT_REPEAT_HOURS, SEARCH_QUERIES
lib/env.tsEnv var names, required-key preflight
lib/db.tsSQLite schema, migrations, retry bookkeeping
lib/youtube.tsStep 1 — trending video discovery
lib/opus.tsStep 2 — AI clipping
lib/tiktok.tsStep 3 — token handling + direct publishing
lib/notify.tsEmail notifications (never throws)
scripts/smoke-test.tsExercises the SQLite layer; safe to run anytime
scripts/send-test-email.tsSends one test email; safe to run anytime

Environment variables

KeyRequired?Where to get it
YOUTUBE_API_KEYyesGoogle Cloud Console → enable YouTube Data API v3 → API key
OPUS_CLIP_API_KEYyesOpus Clip account / API access
TIKTOK_ACCESS_TOKENyes¹TikTok for Developers → Content Posting API (scope video.publish)
NOTIFY_EMAILnoWhere to send notifications. Omit it to mail the val owner's own address.
TIKTOK_CLIENT_KEYoptional²Your TikTok app's client key
TIKTOK_CLIENT_SECREToptional²Your TikTok app's client secret
TIKTOK_REFRESH_TOKENoptional²From the OAuth flow — the seed for automatic renewal

¹ Or use the refresh triple instead — see below. ² Only needed if you want the val to renew its own TikTok token.

The job skips itself and emails you if anything required is missing.

TikTok tokens without a refresh token

If you only have an access token — no refresh_token — that's fully supported. Set TIKTOK_ACCESS_TOKEN and leave the three TIKTOK_CLIENT_* / TIKTOK_REFRESH_TOKEN keys unset.

TikTok access tokens expire in ~24 hours, which is shorter than the gap between runs, so a token will die between runs. When that happens the val:

  1. Recognises it as a credential problem, not a problem with the video, and does not spend one of the video's retry attempts — so a good video isn't parked because your token lapsed.
  2. Remembers a SHA-256 fingerprint of the rejected token in blob storage, so later runs fail immediately without calling TikTok at all.
  3. Emails you once, then re-alerts at most every AUTH_ALERT_REPEAT_HOURS (default 24) instead of twice a day.
  4. Clears all of that automatically the moment a working token appears — just paste a new TIKTOK_ACCESS_TOKEN and the next run resumes.

If you do get a refresh token later, set the three TIKTOK_CLIENT_KEY / TIKTOK_CLIENT_SECRET / TIKTOK_REFRESH_TOKEN keys and the val renews itself indefinitely. TikTok rotates the refresh token on every exchange, so the new pair is cached in blob storage and the env var is only the initial seed.

Retry budget

The table tracks how each video went, so one bad video can't block the pipeline forever:

CREATE TABLE IF NOT EXISTS processed_trending_videos ( video_id TEXT PRIMARY KEY, processed_at DATETIME NOT NULL, status TEXT NOT NULL DEFAULT 'pending', -- 'posted' | 'failed' attempts INTEGER NOT NULL DEFAULT 0, last_error TEXT );
  • status = 'posted' → never retried.
  • status = 'failed' → retried until attempts reaches MAX_ATTEMPTS (default 3, so ≈36 hours of retrying at one run per 12h), then skipped.

The first two columns are the original schema; the other three are added by an idempotent ALTER TABLE migration on startup, so an existing table upgrades itself.

Tune the budget in lib/config.ts. To give a parked video another chance, call forgetProcessed(videoId) — or just delete the row.

Things to know before you turn it on

These are real constraints of the third-party APIs, not bugs in this val.

  1. The Opus Clip endpoint is not a documented public API. POST https://api.opus.pro/v1/clips follows the commonly-referenced contract, but Opus Clip does not publish a stable spec. lib/opus.ts is written to be forgiving — it walks the response for the first plausible clip URL, caption and hashtags, and polls for up to 5 minutes if the render is async — but if your account's contract differs you may need to adjust createBody() and the *_KEYS lists. Everything else in the pipeline stays the same.
  2. TikTok PULL_FROM_URL requires a verified domain. TikTok fetches the clip from the URL Opus returns, so that host must be registered as a verified URL property in your TikTok developer app. If it isn't, the post is rejected.
  3. PUBLIC_TO_EVERYONE requires an audited app. Until your TikTok app passes audit, TikTok forces SELF_ONLY and rejects public posts.
  4. YouTube quota. search.list costs 100 units; the default daily quota is 10,000. With 4 queries × 2 runs/day that's 800 units/day — comfortable, but don't add dozens of keywords.
  5. Opus Clip renders can outlast a run. Polling gives up after 5 minutes; if the render is slower, that run records a failure and the next one retries.

Verifying

  • Run scripts/send-test-email.ts to confirm email delivery works.
  • Run scripts/smoke-test.ts to check the SQLite layer, the retry bookkeeping and the cleanup. It leaves the database untouched.
  • Run main.ts manually from the editor to exercise the full pipeline without waiting for the cron.