Build Wholana
KINDAreplaces $5/mosaves $60/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - 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 - 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DB_PATH | required | ./data/watch.db | Creators, videos, observations and labels. |
SOURCE_API_KEYsecret | required | replace-with-key | Your data source's credential. Priced per run, so treat it as a spending instrument. |
BASELINE_WINDOW_DAYS | required | 90 | How far back the rolling median looks. |
BASELINE_MIN_SAMPLE | required | 8 | Minimum videos before a baseline is computed at all. |
OUTPERFORM_RATIO | required | 2.0 | How far above the median a video must be to be flagged. Set it so a typical day shows a handful. |
LABEL_CAP_PER_RUN | required | 15 | Maximum videos labelled per run, so a source glitch cannot produce a large bill. |
OBSERVATION_RETENTION_DAYS | required | 365 | How long raw observations are kept. Aggregates survive beyond this. |
The build, in order
Deciding what you may collect
You have written down what you are collecting, from where, and under what terms.
Source, permitted use, date checked, and what you will not collect. One page, in the repository.
Files
docs/collection-policy.mdAggregate counts, no identity columns for anyone but the creators you chose. Structural rather than remembered.
Files
src/schema.sqlWhy you are watching them. A watchlist without reasons grows into surveillance by accident.
Files
src/creators.js
done when · tick each as it passeswatch 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.
Collecting nightly, politely
View counts arrive once a night without hammering anything.
One interface. Everything source-specific lives behind it, because the source will change or break.
Files
src/source.jsOne row per video per run. Unique on video and date so a rerun is a no-op.
Files
src/collect.jsWell below any ceiling, with exponential backoff. Politeness here is also what keeps access working.
Files
src/collect.js
done when · tick each as it passeswatch 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.
A baseline per creator
A video is judged against its own creator's usual numbers, not against everyone's.
Over a window in days, per creator. Recompute nightly rather than incrementally, because it is cheap at this size.
Files
src/baseline.jsBelow it, no baseline. A ratio computed from three videos is noise presented as a number.
Files
src/baseline.jsTest with a hand-calculated example so the exclusion is proven rather than assumed.
Files
test/baseline.test.js
done when · tick each as it passeswatch 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.
Detecting and labelling what worked
You see the videos that beat their creator's baseline, with a note on what they did differently.
Views over the creator's baseline, above a threshold you choose. Store the ratio, not just the flag.
Files
src/detect.jsOne model call per outperformer, with a hard cap per run.
Files
src/label.jsYour 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 passeswatch 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.
The page you actually read
A daily page showing what outperformed and why, in under a minute of reading.
Nightly, written to disk. No server, no framework.
Files
src/render.jsBy ratio, grouped by label so patterns are visible across creators.
Files
src/render.jsAn empty page is a true statement. Padding it teaches you to ignore the page.
Files
src/render.js
done when · tick each as it passeswatch 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.
Retention, review and the terms checkproduct builder
The database does not grow into something you would not want to explain, and the terms note stays current.
Delete raw observations past a window; keep baselines and outperformer records, which are small.
Files
src/retention.jsAgainst the recorded reasons. A creator you no longer have a reason to watch comes off the list.
Files
src/review.jsA reminder tied to the date on the note. Terms change and your README makes a claim about them.
Files
docs/collection-policy.md
done when · tick each as it passeswatch out- A collection policy written once and never revisited becomes inaccurate silently, and the terms of these platforms change more often than most. Put a date on the note and a reminder in the calendar, so the claim in your README stays true.
That is the whole plan for Wholana. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A cross-creator corpus and search across it.
- A curated taxonomy built from many accounts.
- Any compliant route to platform data at commercial scale.
- Coverage of creators you did not deliberately choose to watch.
- the cross-creator corpus
- search across videos you never chose to watch
- a curated craft taxonomy instead of labels you invented
- semantic and hybrid search
- subject classification
- creator equity and track-record views
- someone else absorbing the scrape cost and keeping it running
- Growth curve shape as a second signal, since reaching a number in a day differs from three weeks
- Your own label taxonomy replacing the model's, once corrections accumulate
- A weekly digest instead of daily, if the daily page turns out to be mostly empty
Need the files? The project pack on the verdict page hands your agent the whole brief · more social media.