<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: InayaFlow Team</title>
    <description>The latest articles on DEV Community by InayaFlow Team (@inayaflow).</description>
    <link>https://dev.to/inayaflow</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4241990%2F0a9a3f5c-68cb-4e6b-8d0a-0c86a9f2238d.png</url>
      <title>DEV Community: InayaFlow Team</title>
      <link>https://dev.to/inayaflow</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/inayaflow"/>
    <language>en</language>
    <item>
      <title>Building a TikTok shadowban self-check without scraping: help-center rules as typed, tested constants</title>
      <dc:creator>InayaFlow Team</dc:creator>
      <pubDate>Fri, 13 Nov 2026 01:17:32 +0000</pubDate>
      <link>https://dev.to/inayaflow/building-a-tiktok-shadowban-self-check-without-scraping-help-center-rules-as-typed-tested-5cj9</link>
      <guid>https://dev.to/inayaflow/building-a-tiktok-shadowban-self-check-without-scraping-help-center-rules-as-typed-tested-5cj9</guid>
      <description>&lt;p&gt;Search for "TikTok shadowban checker" and you will find tools that ask for a username and return a verdict. We wanted to build one for our site, and the first thing we learned is that there is nothing to look up. TikTok has no public endpoint that reports whether an account is restricted. Its own help pages never use the word "shadowban" at all. They talk about content being "ineligible for recommendation" and about the For You feed (FYF).&lt;/p&gt;

&lt;p&gt;So any outside website that claims to detect a shadowban is guessing. We decided to build something smaller and more honest: a self-check. The user answers questions about what TikTok itself shows them (a notice under their username, a warning sign in Account check, a strike in their inbox), plus whether any of the causes listed in TikTok's own guidelines apply, plus their recent view counts. The tool turns those answers into a verdict and next steps, entirely in the browser. You can try the result here: &lt;a href="https://inayaflow.ai/tools/tiktok-shadowban-checker" rel="noopener noreferrer"&gt;TikTok shadowban self-check&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The interesting engineering problem was not the UI. It was this: &lt;strong&gt;how do you keep a rules-based tool truthful when every rule comes from someone else's documentation?&lt;/strong&gt; Here is the pattern we ended up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraints we set first
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No login, no lookup, no API calls, nothing stored. The logic file has no imports at all.&lt;/li&gt;
&lt;li&gt;Every statement about TikTok must trace back to an exact line on a TikTok page, with the date we read it.&lt;/li&gt;
&lt;li&gt;Exactly one threshold is ours, not TikTok's, and the page labels it as our rule of thumb.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The second constraint drove everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: sources are data, not comments
&lt;/h2&gt;

&lt;p&gt;Instead of a comment like &lt;code&gt;// see TikTok help center&lt;/code&gt;, every source lives in a typed constant with its URL, the date we read it and the exact lines we rely on. A trimmed excerpt from &lt;code&gt;lib/shadowbanCheck.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;Source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
  &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
  &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
  &lt;span class="cm"&gt;/** Date we read the page (YYYY-MM-DD). */&lt;/span&gt;
  &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
  &lt;span class="cm"&gt;/** Exact lines the page is cited for. */&lt;/span&gt;
  &lt;span class="n"&gt;quotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;SOURCES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;accountStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'accountStatus'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'TikTok Help Center'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Your account status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'https://www.tiktok.com/en/safety-hc/account-and-user-safety/account-status'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'2026-11-13'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;quotes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s1"&gt;'In the TikTok app, tap Profile at the bottom.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'Tap the Menu ☰ button at the top, then select TikTok Studio.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'Within More tools, tap Account check.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// notRecommended, violations, fyf, integrity, recommendation, tips&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;satisfies&lt;/span&gt; &lt;span class="nc"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Source&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;SourceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="no"&gt;SOURCES&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;satisfies&lt;/code&gt; is doing real work here. It checks every entry against &lt;code&gt;Source&lt;/code&gt;, but keeps the literal keys, so &lt;code&gt;SourceId&lt;/code&gt; becomes a union of the seven ids rather than plain &lt;code&gt;string&lt;/code&gt;. Any question or result step that cites a source has to name one that exists, and the compiler enforces it.&lt;/p&gt;

&lt;p&gt;The page renders its "Sources" list straight from &lt;code&gt;Object.values(SOURCES)&lt;/code&gt;, so the list a reader sees and the list the code cites can't drift apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: every question and every step carries a source id
&lt;/h2&gt;

&lt;p&gt;The ten questions fall into three groups:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/** notice = something TikTok shows you itself;
 *  cause = a cause TikTok names in its rules;
 *  context = not a restriction. */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;QuestionGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'notice'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s1"&gt;'cause'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="s1"&gt;'context'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nc"&gt;Question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;QuestionId&lt;/span&gt;
  &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;QuestionGroup&lt;/span&gt;
  &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
  &lt;span class="n"&gt;hint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
  &lt;span class="n"&gt;unsureLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="c1"&gt;// "Not checked", "Not sure", "Didn't test"&lt;/span&gt;
  &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;SourceId&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result steps use the same idea: &lt;code&gt;type Step = { text: string; source?: SourceId }&lt;/code&gt;. The widget renders each step with a link to its source's URL and publisher, and a test checks that every question cites a source that exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: an explicit priority order for verdicts
&lt;/h2&gt;

&lt;p&gt;The scoring is deliberately boring. A simplified version of &lt;code&gt;assess()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified from assess() — the real one also builds steps and summaries.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;told&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'told'&lt;/span&gt;           &lt;span class="c1"&gt;// TikTok showed a notice&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;causes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;searchMiss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'signs'&lt;/span&gt;          &lt;span class="c1"&gt;// a cause TikTok names&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bigDrop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'drop-no-signs'&lt;/span&gt;  &lt;span class="c1"&gt;// views fell, nothing else&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'no-signs'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notices beat causes, causes beat view drops. Two details matter more than they look:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Not sure" never counts as yes.&lt;/strong&gt; Unanswered notice questions are returned as a separate &lt;code&gt;notChecked&lt;/code&gt; list, and the summary sends the user to check those first. A self-check that guesses on the user's behalf is just a scraper with extra steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The one number of our own is isolated.&lt;/strong&gt; &lt;code&gt;BIG_DROP_RATIO = 0.5&lt;/code&gt;: the median of up to ten recent videos is compared with the user's usual views. We use the median so one dud video doesn't swing the result. A test pins the edge case: exactly half is &lt;em&gt;not&lt;/em&gt; "under half".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When views fall but nothing else applies, the result quotes TikTok's own line that a video that isn't getting many views "doesn't necessarily mean it broke a rule", and points to practical checks instead. We wrote those up separately in a guide on &lt;a href="https://inayaflow.ai/blog/why-is-my-tiktok-getting-no-views" rel="noopener noreferrer"&gt;why a TikTok video gets no views&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 4: tests that fact-check the copy
&lt;/h2&gt;

&lt;p&gt;This is the part we'd reuse on any docs-driven tool. In-app paths ("Profile → Menu ☰ → TikTok Studio → ...") are the most likely thing to go stale or be typed from memory. So paths are constants too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="no"&gt;PATHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;studio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Profile → Menu ☰ → TikTok Studio → More tools → Account check'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'Inbox → System notifications → Account updates'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a test walks every step of every path and requires it to appear inside a quoted line of its source, in TikTok's order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'every step of every in-app path is inside a quoted line of its TikTok source'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;PATHS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;quotes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;SOURCES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;PATH_SOURCE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;quotes&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' → '&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quotes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sb"&gt;`${key}: "${step}" is not in ${PATH_SOURCE[key]}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A second test reads the page, widget and copy files as text and fails if an arrow character appears anywhere outside the &lt;code&gt;PATHS&lt;/code&gt; definitions. You can't hand-type a path into the UI; you have to go through a constant that the first test has already checked.&lt;/p&gt;

&lt;p&gt;This came out of a failed internal review of our first version. The reviewer found paths we couldn't tie to a quoted line, one TikTok line cited to the wrong page, and one sentence that was our own opinion phrased like a fact. The fix was to re-read the three Help Center pages in a headless browser, copy the exact lines into &lt;code&gt;quotes&lt;/code&gt;, rebuild the paths from them, and add the tests above so the same class of mistake fails &lt;code&gt;npm test&lt;/code&gt; next time.&lt;/p&gt;

&lt;p&gt;One more wrinkle: what our team saw in the app on 8 Nov 2026 (account status under Settings and privacy) didn't match the Help Center's paths. We kept it, but in a separate &lt;code&gt;SEEN_IN_APP&lt;/code&gt; constant with a fixed label, "What we saw in the TikTok app on 8 Nov 2026". A test checks that the label always travels with the path, so an observation never gets presented as TikTok's wording.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 5: honesty as assertions
&lt;/h2&gt;

&lt;p&gt;Some tests look odd until you think of them as a contract with the reader:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No source quote may contain "shadow". We never put the word in TikTok's mouth.&lt;/li&gt;
&lt;li&gt;The limits line ("This self-check can't see TikTok's systems, and TikTok doesn't confirm shadowbans.") must render twice in the widget: before the questions and inside every result.&lt;/li&gt;
&lt;li&gt;A regex rejects promise language such as "go viral", "guarantee" or "unban" across the page, widget, logic and copy files.&lt;/li&gt;
&lt;li&gt;The widget file must start with &lt;code&gt;'use client'&lt;/code&gt; and must not contain &lt;code&gt;fetch(&lt;/code&gt;, &lt;code&gt;/api/&lt;/code&gt;, &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;sessionStorage&lt;/code&gt;, &lt;code&gt;document.cookie&lt;/code&gt; or &lt;code&gt;indexedDB&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Grepping your own source files is brittle, and we accept that. These checks are cheap, they run on every &lt;code&gt;npm test&lt;/code&gt;, and each one encodes a promise the page makes to the person using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small parsing problem: what does "1,200, 300" mean?
&lt;/h2&gt;

&lt;p&gt;Users paste view counts in every format. The rule we settled on: if the text contains spaces, semicolons or new lines, those separate the numbers and commas are thousands separators. With no spaces at all, commas separate. So &lt;code&gt;"1,200, 300"&lt;/code&gt; is &lt;code&gt;[1200, 300]&lt;/code&gt; and &lt;code&gt;"150,200,90"&lt;/code&gt; is &lt;code&gt;[150, 200, 90]&lt;/code&gt;. &lt;code&gt;1.2K&lt;/code&gt; and &lt;code&gt;3M&lt;/code&gt; work; decimals that don't land on a whole number are rejected with a readable error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running TypeScript tests with plain &lt;code&gt;node --test&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The project's test script is just &lt;code&gt;node --test tests/*.test.mjs&lt;/code&gt;. To import &lt;code&gt;.ts&lt;/code&gt; lib files without a test framework, a small helper registers Node module hooks (&lt;code&gt;registerHooks&lt;/code&gt; from &lt;code&gt;node:module&lt;/code&gt;) that resolve the &lt;code&gt;@/&lt;/code&gt; alias and transpile &lt;code&gt;.ts&lt;/code&gt; with the project's own &lt;code&gt;typescript&lt;/code&gt; package. The test file then does a plain &lt;code&gt;await import('../lib/shadowbanCheck.ts')&lt;/code&gt;. No Jest, no Vitest, no extra build step.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd tell anyone building a docs-driven tool
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Store quotes, URLs and read dates as typed data, and render your citations from that same data.&lt;/li&gt;
&lt;li&gt;Make the compiler reject citations to sources that don't exist.&lt;/li&gt;
&lt;li&gt;Build anything a user might copy (paths, menu names) from quoted lines, and test that it stays that way.&lt;/li&gt;
&lt;li&gt;Label your own numbers and observations as yours, in code, not only in prose.&lt;/li&gt;
&lt;li&gt;Read dates are a reminder, not a guarantee: help pages change, and the quotes need a fresh read when they do.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;We build InayaFlow, free TikTok tools for creators. The self-check described here is one of them, at &lt;a href="https://inayaflow.ai" rel="noopener noreferrer"&gt;inayaflow.ai&lt;/a&gt;. Questions about the approach are welcome in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>testing</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
