Build Wholana

KINDAreplaces $5/mosaves $60/yrback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A nightly job that records view counts for a small watchlist of creators, computes each creator's rolling median so a video is judged against their own baseline rather than against a global one, surfaces the ones that outperformed, and labels what those videos did differently.

estimated effort weekendthe files for this build are in the project pack

RuntimeNode 22 with SQLite through better-sqlite3CollectionA single source behind one adapter, polled nightlyBaselineRolling median of a creator's recent videosLabellingA model applied to the outperformers onlyInterfaceA static page generated nightly

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the nightly job and the page generator.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. decidefree, and it is the first task rather than a formality

    Why This decides whether you build at all and what shape it takes. TikTok's Research API is limited to non-profit academic researchers at qualifying institutions and excludes commercial interests; the platform terms prohibit automated collection.

    Get it Read the Research API eligibility page and the platform terms. Decide whether you qualify, and if not, decide deliberately what you are willing to do and write it down with the date. open ↗

    Verify A written note names the source, the permitted use and the date checked

  3. API keythe Research API is free for those eligible; third party services charge per run and add up with a nightly schedule

    Why Without it there is nothing to collect. Third party scraping services are the usual route and are priced per run.

    Get it If you qualify for the Research API, apply; the process takes up to about four weeks. Otherwise, evaluate a third party service and understand that its terms exposure does not fully replace yours.

    Verify A test run returns video data for one creator

  4. decidefree

    Why A small, reasoned watchlist is a different thing from broad collection, and the reasons are what keep it that way over time.

    Get it List the creators you already follow for a reason, and write the reason next to each. If you cannot write a reason, leave them off.

    Verify Every creator on the list has a recorded reason

  5. API keypay per token; labelling a handful of videos a night is negligible, labelling everything is not

    Why Phase 4 only. Detection works without it and labelling is a convenience on top.

    Get it A provider key with a spend limit set at the provider.

    Verify A test label returns and its cost is recorded

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- creators(id, handle, added_at, reason, enabled) · reason is why you are watching them, which keeps the watchlist honest
- videos(id, creator_id, external_id, posted_at, caption, duration_s, first_seen_at)
- metrics(video_id, observed_at, views, likes, comments) · one row per observation, so growth over time is visible
- baselines(creator_id, computed_at, window_days, median_views, sample_size)
- outperformers(video_id, ratio, detected_at, labels_json, reviewed) · ratio is views over the creator's median
- Aggregate counts only. No follower lists, no commenter identities, no profiles of people who did not choose to be in your database.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DB_PATHrequired./data/watch.dbCreators, videos, observations and labels.
SOURCE_API_KEYsecretrequiredreplace-with-keyYour data source's credential. Priced per run, so treat it as a spending instrument.
BASELINE_WINDOW_DAYSrequired90How far back the rolling median looks.
BASELINE_MIN_SAMPLErequired8Minimum videos before a baseline is computed at all.
OUTPERFORM_RATIOrequired2.0How far above the median a video must be to be flagged. Set it so a typical day shows a handful.
LABEL_CAP_PER_RUNrequired15Maximum videos labelled per run, so a source glitch cannot produce a large bill.
OBSERVATION_RETENTION_DAYSrequired365How long raw observations are kept. Aggregates survive beyond this.

The build, in order

  1. Deciding what you may collect

    You have written down what you are collecting, from where, and under what terms.

    1. Source, permitted use, date checked, and what you will not collect. One page, in the repository.

      Files docs/collection-policy.md

    2. Aggregate counts, no identity columns for anyone but the creators you chose. Structural rather than remembered.

      Files src/schema.sql

    3. Why you are watching them. A watchlist without reasons grows into surveillance by accident.

      Files src/creators.js

    done when · tick each as it passes
    watch out
    • Building the collector first and deciding the policy afterwards produces a system you are not comfortable running, and by then the schema already holds data you did not want. Deciding first is cheaper and the schema then enforces it: if there is no column for follower identities, nothing can accidentally store them.
  2. Collecting nightly, politely

    View counts arrive once a night without hammering anything.

    1. One interface. Everything source-specific lives behind it, because the source will change or break.

      Files src/source.js

    2. One row per video per run. Unique on video and date so a rerun is a no-op.

      Files src/collect.js

    3. Well below any ceiling, with exponential backoff. Politeness here is also what keeps access working.

      Files src/collect.js

    done when · tick each as it passes
    watch out
    • Updating a video's view count in place loses the growth curve, which is the interesting part: a video that reached its number in a day is a different signal from one that took three weeks. Append observations; never overwrite them.
  3. A baseline per creator

    A video is judged against its own creator's usual numbers, not against everyone's.

    1. Over a window in days, per creator. Recompute nightly rather than incrementally, because it is cheap at this size.

      Files src/baseline.js

    2. Below it, no baseline. A ratio computed from three videos is noise presented as a number.

      Files src/baseline.js

    3. Test with a hand-calculated example so the exclusion is proven rather than assumed.

      Files test/baseline.test.js

    done when · tick each as it passes
    watch out
    • A mean is dragged upward by one viral video and stays high for months, so nothing after it ever looks like an outperformer. Median is resistant to exactly that.
    • Including the video in its own baseline dampens the very signal you are measuring, and it matters most for creators with few videos, which is where you most want the signal.
  4. Detecting and labelling what worked

    You see the videos that beat their creator's baseline, with a note on what they did differently.

    1. Views over the creator's baseline, above a threshold you choose. Store the ratio, not just the flag.

      Files src/detect.js

    2. One model call per outperformer, with a hard cap per run.

      Files src/label.js

    3. Your corrections are the training signal for your own label set, and they are more accurate than the model's first pass.

      Files src/label.js

    done when · tick each as it passes
    watch out
    • Labelling every video means paying for thousands of labels you never read, and the ones that matter are a small fraction. Detect first, label second, and cap the number labelled per run so a source glitch that flags everything cannot produce a large bill.
  5. The page you actually read

    A daily page showing what outperformed and why, in under a minute of reading.

    1. Nightly, written to disk. No server, no framework.

      Files src/render.js

    2. By ratio, grouped by label so patterns are visible across creators.

      Files src/render.js

    3. An empty page is a true statement. Padding it teaches you to ignore the page.

      Files src/render.js

    done when · tick each as it passes
    watch out
    • A page listing every video with a ratio above one is a page nobody reads, because most days nothing interesting happened. Set the threshold so a typical day shows a handful, and let an empty day be empty rather than padding it.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more social media.