TikTok Money Calculator

Last reviewed: Free calculator No sign-up Runs in your browser

TikTok Money Calculator 2026

Connect TikTok for creator insights and content ideas, or use manual monthly views to estimate a TikTok Creator Rewards scenario. Zenith starts from a transparent $0.70 RPM planning baseline and uses modest market and niche modifiers instead of treating them as separate payout rates.

UNISONICA • CREATOR TOOLS, DATA & GROWTH • ZENITH v19.5.5 PRODUCTION

Connect your TikTok account Primary tool

Connect an account you control through TikTok. Zenith can import a public-video sample, compare snapshots and generate practical creator tips. Older videos are included when TikTok returns them. Manual entry always remains available.

Official TikTok authorization. Unisonica never asks for your TikTok password. OAuth tokens stay server-side, and account responses may be cached for up to 5 minutes to reduce unnecessary TikTok API calls.
Estimated monthly Creator Rewards
CREATOR REWARDS MODEL
Lower scenario $0
Base estimate $0
Upper scenario $0
Annual estimate
$0
Effective model RPM
$0.00
Planning range ±20%
$0 – $0
Qualified views modeled

Important: This is an independent Creator Rewards planning calculator, not an official TikTok payout tool and not a guarantee of earnings. Actual rewards can be lower or higher depending on eligibility, qualified views, audience location, content performance, platform rules and other factors.

⚡ Build your scenario

Use a realistic monthly view count rather than your biggest viral month.
Small planning modifiers only: +15%, neutral, or -15%. They are not official TikTok RPM rates.
Niche changes the scenario modestly; it does not create a separate payout rate.
Used for goal progress and required-view estimates only.
⚙ Advanced assumptions
Lower this when only part of your total views should be treated as qualified. If you have Creator Rewards analytics, use your own recent qualified-view share.
Leave this at 100% for the modeled RPM. Reduce it only to stress-test a weaker month.

How to read your TikTok earnings estimate

Start with a normal 28- to 30-day view count. Zenith then applies a $0.70 Creator Rewards RPM planning baseline, adjusts it modestly for audience market and niche, and finally applies your qualified-view share and optional scenario adjustment. If you already have Creator Rewards data, your own history should take priority over the defaults.

What each Creator Rewards model input changes
InputWhat it representsPractical tip
Base RPMZenith starts at $0.70 per 1,000 qualified views before market and niche modifiers.This is an independent planning baseline, not an official TikTok rate.
Monthly viewsYour expected total views in a typical month.Use a recent 28- to 30-day period instead of one unusually viral month.
Audience marketA broad scenario for where most of your viewers are located.Choose the market that best matches the majority of your audience, not your own location alone.
Content nicheA small modifier around the $0.70 baseline for the commercial context of your content.Pick the category that describes most of your uploads rather than one exceptional video.
Qualified viewsThe share of total views you want the model to treat as monetizable.If you have Creator Rewards analytics, use your own qualified-view history as the reference.
Scenario adjustmentAn optional reduction applied after the modeled RPM is calculated.Keep it at 100% for the base model; lower it only when testing a weaker scenario.

How this TikTok earnings calculator works

Zenith uses a transparent Creator Rewards model rather than multiplying large country and niche modifiers together. It starts with a $0.70 RPM planning baseline, applies a modest audience-market modifier and niche modifier, then multiplies the result by your qualified views and optional scenario adjustment. The result is a planning estimate, not a promise of what TikTok will pay.

Model formula: qualified views ÷ 1,000 × [$0.70 base RPM × market modifier × niche modifier] × scenario adjustment.

01Start with viewsEnter a realistic monthly view count or use a quick preset.
02Adjust the baselineMarket and niche make modest percentage changes around the $0.70 RPM baseline.
03Stress-test the resultUse qualified views, the scenario adjustment and the ±20% range to compare outcomes.

What the estimate does not include

The earnings model cannot access your private Creator Rewards payout data, eligibility status, exact audience mix, retention or engagement-quality signals. When you explicitly authorize TikTok, Zenith can separately import the profile and public-video metrics that TikTok makes available through its API. The earnings estimate also does not include sponsorships, affiliate income, TikTok Shop revenue or taxes, all of which can make real-world income differ from the model.

TikTok earnings calculator FAQ

Can Zenith analyze any TikTok account automatically?

Detailed automatic statistics require authorization from the account owner through TikTok. Zenith can analyze an account you control after you connect it, while the manual calculator remains available for any scenario you want to model.

Are imported TikTok views the same as monthly views?

No. TikTok exposes current lifetime metrics for the public videos returned by the API, not a day-by-day account history. Zenith can import up to 100 of the latest public videos regardless of upload age. Treat that imported total as a creator-intelligence sample; use your own recent analytics when you need an actual monthly-view figure.

Is this an official TikTok calculator?

No. Zenith by Unisonica is an independent estimator, not a TikTok product. Use the result as a planning scenario rather than an official payout quote.

What RPM baseline does Zenith use?

Zenith starts from an independent $0.70 per 1,000 qualified views planning baseline. Market and niche only make modest percentage adjustments around that baseline, and your own Creator Rewards history should replace the default assumptions once you have enough data.

Why can two creators with the same views earn different amounts?

Because views are only one part of the picture. Audience location, qualified views, eligibility, content category, watch behavior and advertiser demand can all change the outcome.

What does “effective model RPM” mean here?

It is the estimated revenue per 1,000 total views after the $0.70 base RPM, market modifier, niche modifier, qualified-view share and scenario adjustment are applied. It is a model output, not an official TikTok RPM.

Should I use total views or qualified views?

Enter total monthly views in the main field. If you want to model only the portion likely to qualify, open Advanced assumptions and lower the qualified-view percentage.

What does the growth projection mean?

The growth projection compounds the monthly growth rate you choose and applies the same assumptions to the projected views. It is a what-if exercise, not a forecast.

What does “Compare niches” do?

It runs the same Creator Rewards scenario across the available niche modifiers while keeping your other inputs unchanged. That makes it useful for comparison, but it does not mean switching niches will automatically produce those earnings.

'; downloadBlob('\ufeff' + html, 'application/msword', 'unisonica-tiktok-earnings-estimate.doc'); showToast('Word report exported.'); trackEvent('zenith_export', { format: 'word' }); }async function copyText(text) { if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(text); return; } const area = document.createElement('textarea'); area.value = text; area.setAttribute('readonly',''); area.style.position = 'fixed'; area.style.opacity = '0'; document.body.appendChild(area); area.select(); document.execCommand('copy'); area.remove(); }async function copyReport() { try { await copyText(getCleanReport()); showToast('Estimate copied to clipboard.'); trackEvent('zenith_copy'); return true; } catch (_) { showToast('Could not copy automatically.'); return false; } }function buildShareURL() { const values = getValues(); const params = new URLSearchParams({ v: String(Math.round(values.views)), t: el.tier.value, n: el.niche.value, g: String(Math.round(values.goal)), f: String(Math.round(values.factorPercent)), q: String(Math.round(values.qualifiedPercent)), gr: String(Math.round(values.growthPercent)), zv: ZENITH_VERSION, model: ZENITH_MODEL_VERSION }); const url = new URL(window.location.href); url.hash = 'zenith=' + encodeURIComponent(params.toString()); return url.toString(); }function readHashState() { const hash = window.location.hash || ''; if (!hash.startsWith('#zenith=')) return null; try { const raw = decodeURIComponent(hash.slice('#zenith='.length)); const p = new URLSearchParams(raw); return { views:p.get('v'), tier:p.get('t'), niche:p.get('n'), goal:p.get('g'), factor:p.get('f'), qualified:p.get('q'), growth:p.get('gr') }; } catch (_) { return null; } }async function shareReport() { const url = buildShareURL(); const text = 'My TikTok earnings scenario from Unisonica Zenith: ' + el.base.textContent + ' estimated monthly.'; if (navigator.share) { try { await navigator.share({ title:'TikTok Money Calculator 2026 — Unisonica', text, url }); trackEvent('zenith_share', { method:'native' }); return; } catch (error) { if (error && error.name === 'AbortError') return; } } try { await copyText(url); showToast('Shareable scenario link copied.'); trackEvent('zenith_share', { method:'clipboard' }); } catch (_) { await copyReport(); } }function printZenith() { document.body.classList.add('u-zenith-printing'); const cleanup = () => document.body.classList.remove('u-zenith-printing'); window.addEventListener('afterprint', cleanup, { once:true }); window.print(); window.setTimeout(cleanup, 1500); trackEvent('zenith_export', { format:'pdf_print' }); }function showToast(message) { if (!el.toast) return; el.toast.textContent = message; el.toast.classList.add('is-visible'); clearTimeout(toastTimer); toastTimer = window.setTimeout(() => el.toast.classList.remove('is-visible'), 2200); }/* ================================================================ OPTIONAL OFFICIAL TIKTOK ACCOUNT IMPORT Backend endpoints are provided by the companion WordPress PHP snippet. Failure here never blocks the manual calculator. ================================================================ */ const TT_API = Object.freeze({ connect: '/wp-json/unisonica/v1/tiktok/connect', account: '/wp-json/unisonica/v1/tiktok/account', disconnect: '/wp-json/unisonica/v1/tiktok/disconnect' });let ttImportedViews = 0; let ttLastPayload = null; let ttActiveWindow = 'all'; let ttAccountLoadInFlight = false;function ttSetStatus(message, state = 'info') { if (!el.ttStatus) return; el.ttStatus.textContent = message || ''; el.ttStatus.dataset.state = state; el.ttStatus.classList.toggle('is-visible', Boolean(message)); }function ttSetBusy(busy) { [el.ttConnect, el.ttSyncNow, el.ttRefresh, el.ttDisconnect, el.ttProfileStatsButton].filter(Boolean).concat($$('.z-tt-window-btn')).forEach((button) => { button.disabled = Boolean(busy); }); }function ttCleanReturnURL() { try { const url = new URL(window.location.href); url.searchParams.delete('uni_tt'); url.searchParams.delete('uni_tt_error'); url.searchParams.delete('uni_tt_reason'); return url.toString(); } catch (_) { return window.location.href; } }function ttSigned(value) { const n = Number(value || 0); if (!Number.isFinite(n)) return '—'; if (n === 0) return '0'; return (n > 0 ? '+' : '−') + number0.format(Math.abs(n)); }function ttApiErrorMessage(payload, status) { const message = payload && payload.message ? String(payload.message) : ''; const data = payload && payload.data && typeof payload.data === 'object' ? payload.data : {}; const code = String(data.tiktok_code || '').toLowerCase(); const retryAfter = Math.max(0, Number(data.retry_after || 0) || 0);if (status === 429 || code === 'rate_limit_exceeded' || code === 'local_rate_limit') { return retryAfter > 0 ? 'TikTok statistics were requested too quickly. Try again in about ' + Math.ceil(retryAfter) + ' seconds.' : 'TikTok is temporarily rate-limiting requests. Please try again shortly.'; } if (status === 401 || code === 'access_token_invalid' || code === 'scope_not_authorized') { return message || 'TikTok authorization needs attention. Reconnect the account if this continues.'; } if (status >= 500) { return 'TikTok is temporarily unavailable. Your existing calculator and any already loaded account snapshot remain usable.'; } return message || 'TikTok import is unavailable right now. You can continue with manual values.'; }function ttBuildAdvice(stats, progress, windowKey = 'all') { if (!el.ttAdviceList) return; const videos = Math.max(0, Number(stats.imported_videos !== undefined ? stats.imported_videos : (stats.recent_videos_30d || 0)) || 0); const views = Math.max(0, Number(stats.imported_views !== undefined ? stats.imported_views : (stats.recent_upload_views_30d || 0)) || 0); const likes = Math.max(0, Number(stats.imported_likes !== undefined ? stats.imported_likes : (stats.recent_upload_likes_30d || 0)) || 0); const comments = Math.max(0, Number(stats.imported_comments !== undefined ? stats.imported_comments : (stats.recent_upload_comments_30d || 0)) || 0); const shares = Math.max(0, Number(stats.imported_shares !== undefined ? stats.imported_shares : (stats.recent_upload_shares_30d || 0)) || 0); const interactions = likes + comments + shares; const rate = views > 0 ? (interactions / views) * 100 : 0; const avgViews = videos > 0 ? views / videos : 0; const tips = [];if (videos === 0) { if (windowKey === 'all') { tips.push('No public videos were returned in the imported sample. If the account has public videos, refresh once; otherwise publish public content to build a baseline.'); tips.push('A zero imported view count is valid when TikTok returns no usable public-video metrics. Manual entry remains available for separate what-if scenarios.'); } else { const days = windowKey === '7d' ? '7 days' : '28 days'; tips.push('No imported public videos were published during the selected ' + days + ' window. Try All time for the broader account sample.'); tips.push('This period filter is based on video publish date. TikTok still returns each included video\'s current lifetime metrics, not views received only during that period.'); } } else { if (videos < 4) tips.push('The imported sample is small. Treat comparisons cautiously until TikTok returns more public videos.'); if (videos >= 8) tips.push('The imported sample is large enough to compare formats. Repeat the hooks and topics that consistently beat your own average.'); if (avgViews < 1000) tips.push('Average views per imported video are modest. Test clearer first-second hooks, specific searchable topics and faster delivery of the main value in future posts.'); else tips.push('Your imported sample is large enough to compare top videos against your typical video instead of judging the account from one viral result.'); if (views > 0 && rate < 1) tips.push('Interaction density is below 1% in this imported snapshot. Test stronger questions, save-worthy details and more shareable takeaways in future posts.'); if (views > 0 && rate >= 4) tips.push('Interaction density is strong in this imported snapshot. Preserve the formats that generate comments and shares while testing new hooks around the same topics.'); }if (progress && progress.has_previous && Number(progress.views_delta || 0) > 0) { tips.push('Views increased since the previous saved sample snapshot. Compare the same imported sample over time before treating the change as a trend.'); }el.ttAdviceList.innerHTML = ''; tips.slice(0, 4).forEach((tip) => { const li = document.createElement('li'); li.textContent = tip; el.ttAdviceList.appendChild(li); }); }function ttSyncCalculatorFromAccount() { if (!el.views) return; el.views.value = String(Math.round(ttImportedViews)); runZenith(); showToast('Connected-account views synced to the calculator.'); }function ttShowAccountLoading() { if (!el.ttProfile) return; if (el.ttAccountCard) el.ttAccountCard.hidden = false; el.ttProfile.hidden = false; if (el.ttName) el.ttName.textContent = 'Loading TikTok account…'; if (el.ttProfileSub) el.ttProfileSub.textContent = 'Authorization completed • fetching profile and public-video sample'; [el.ttViews, el.ttVideos, el.ttAvgViews, el.ttInteractions, el.ttInteractionRate, el.ttSynced].filter(Boolean).forEach(function(node) { node.textContent = '…'; }); [el.ttFollowersCard, el.ttProfileLikesCard, el.ttTotalVideosCard, el.ttFollowingCard].filter(Boolean).forEach(function(card) { card.hidden = true; }); if (el.ttProgressState) el.ttProgressState.textContent = 'Loading snapshot'; if (el.ttDeltaViews) el.ttDeltaViews.textContent = '—'; if (el.ttDeltaInteractions) el.ttDeltaInteractions.textContent = '—'; if (el.ttDeltaPosts) el.ttDeltaPosts.textContent = '—'; }function ttShowSessionUnavailable() { if (!el.ttProfile) return; if (el.ttAccountCard) el.ttAccountCard.hidden = false; el.ttProfile.hidden = false; if (el.ttName) el.ttName.textContent = 'TikTok session not available'; if (el.ttProfileSub) el.ttProfileSub.textContent = 'Authorization returned, but the server session could not be restored. Use Connect TikTok again.'; [el.ttViews, el.ttVideos, el.ttAvgViews, el.ttInteractions, el.ttInteractionRate, el.ttSynced].filter(Boolean).forEach(function(node) { node.textContent = '—'; }); }function ttWindowLabel(key) { if (key === '7d') return 'Last 7 days'; if (key === '28d') return 'Last 28 days'; return 'All time'; }function ttWindowNote(key) { if (key === '7d') return "Videos published in the last 7 days. Metrics are each video's current lifetime totals, not views received only during 7 days."; if (key === '28d') return "Videos published in the last 28 days. Metrics are each video's current lifetime totals, not views received only during 28 days."; return "All time uses the full imported sample (up to 100 latest public videos). Metrics are each video's current lifetime totals."; }function ttGetWindowBundle(payload, requestedKey) { const windows = payload && payload.stats_windows && typeof payload.stats_windows === 'object' ? payload.stats_windows : {}; const progressWindows = payload && payload.progress_windows && typeof payload.progress_windows === 'object' ? payload.progress_windows : {}; const topWindows = payload && payload.top_videos_windows && typeof payload.top_videos_windows === 'object' ? payload.top_videos_windows : {}; const key = windows[requestedKey] ? requestedKey : 'all'; return { key, stats: windows[key] || (payload && payload.stats) || {}, progress: progressWindows[key] || (key === 'all' ? ((payload && payload.progress) || {}) : {}), topVideos: topWindows[key] || (key === 'all' ? ((payload && payload.top_videos) || []) : []) }; }function ttUpdateWindowControls(key) { $$('.z-tt-window-btn').forEach((button) => { const active = button.dataset.window === key; button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', String(active)); }); if (el.ttWindowNote) el.ttWindowNote.textContent = ttWindowNote(key); }/* v19.5.5: creator ideas are rendered by the primary TikTok script itself. This intentionally does not depend on the optional second script, CustomEvent timing, cache order or a late listener. */ function ttRenderCreatorIdeasInline(payload, currentTopVideos, windowKey = ttActiveWindow) { try { const list = root.querySelector('#v19-content-ideas-list'); if (!list || !payload || !payload.connected) return false;const ideasRoot = root.querySelector('#v19-content-ideas'); const sourceNote = ideasRoot ? ideasRoot.querySelector('.z-ci-ideas-note') : null; const activeKey = windowKey === '7d' || windowKey === '28d' ? windowKey : 'all'; const activeLabel = ttWindowLabel(activeKey); const currentVideos = Array.isArray(currentTopVideos) ? currentTopVideos : []; const videos = currentVideos;// v19.5.5: strict window purity. Never reuse All time topics while a // 7-day or 28-day window is selected. If that period has no videos, // render an explicit empty state so the UI can never imply that older // topics belong to the selected period. if (!videos.length && activeKey !== 'all') { const emptyCard = document.createElement('div'); const strong = document.createElement('strong'); const paragraph = document.createElement('p'); emptyCard.className = 'z-ci-idea z-ci-idea--empty'; strong.textContent = 'No videos in ' + activeLabel; paragraph.textContent = 'No public videos were published in this period. Switch to All time to generate ideas from your broader imported TikTok sample.'; emptyCard.appendChild(strong); emptyCard.appendChild(paragraph); list.replaceChildren(emptyCard); if (sourceNote) { sourceNote.textContent = 'Ideas are intentionally not borrowed from All time while ' + activeLabel + ' is selected. This keeps creator recommendations aligned with the active statistics period.'; } list.dataset.renderedBy = 'zenith-primary'; list.dataset.sourceWindow = activeKey + '-empty'; return true; }if (sourceNote) { sourceNote.textContent = 'Ideas are based on the selected ' + activeLabel + ' imported-video sample. Generated locally in your browser; no external AI API is used.'; }const subjects = []; videos.slice(0, 5).forEach((video) => { const raw = String((video && (video.title || video.description)) || '').replace(/\s+/g, ' ').trim(); if (!raw) return; const clean = raw.length > 82 ? raw.slice(0, 79) + '...' : raw; if (!subjects.includes(clean)) subjects.push(clean); });if (!subjects.length) { const niche = root.querySelector('#v16-niche'); const fallback = niche && niche.options && niche.selectedIndex >= 0 ? String(niche.options[niche.selectedIndex].textContent || 'your niche').trim() : 'your niche'; subjects.push(fallback || 'your niche'); if (sourceNote) { sourceNote.textContent = activeKey === 'all' ? 'TikTok returned no usable public-video topics in the imported sample, so ideas use your selected niche. Generated locally in your browser; no external AI API is used.' : ('No videos were published in ' + activeLabel + ' and no broader TikTok topics were available, so ideas use your selected niche. Generated locally in your browser; no external AI API is used.'); } }const templates = [ ['Sharper remake', 'Make a clearer, faster version of: ', 'Open with the result first, then explain only the steps needed to reach it.'], ['Mistakes format', '3 mistakes people make with: ', 'Use three concrete mistakes and finish with one simple correction viewers can try today.'], ['Proof first', 'Before you try this: ', 'Show the strongest proof or outcome in the first seconds, then explain why it worked.'], ['Comment magnet', 'Would you choose this approach? ', 'Present two reasonable options and ask viewers which one they would choose and why.'], ['Personal lesson', 'What I wish I knew earlier about: ', 'Turn the topic into one short lesson plus two practical takeaways.'], ['Follow-up', 'The part nobody explains about: ', 'Build a follow-up around one unanswered detail from the original topic.'] ];const frag = document.createDocumentFragment(); templates.forEach((template, index) => { const subject = subjects[index % subjects.length]; const card = document.createElement('div'); const strong = document.createElement('strong'); const paragraph = document.createElement('p'); card.className = 'z-ci-idea'; strong.textContent = template[0] + ' — ' + template[1] + subject; paragraph.textContent = template[2]; card.appendChild(strong); card.appendChild(paragraph); frag.appendChild(card); });list.replaceChildren(frag); list.dataset.renderedBy = 'zenith-primary'; list.dataset.sourceWindow = activeKey; return true; } catch (_) { return false; } }function ttRenderActiveWindow({ sync = false, dispatch = true } = {}) { if (!ttLastPayload || !ttLastPayload.connected || !el.ttProfile) return false;const profile = ttLastPayload.profile || {}; const bundle = ttGetWindowBundle(ttLastPayload, ttActiveWindow); ttActiveWindow = bundle.key; const stats = bundle.stats || {}; const progress = bundle.progress || {};ttImportedViews = Math.max(0, Number(stats.imported_views !== undefined ? stats.imported_views : (stats.recent_upload_views_30d || 0)) || 0); const videos = Math.max(0, Number(stats.imported_videos !== undefined ? stats.imported_videos : (stats.recent_videos_30d || 0)) || 0); const likes = Math.max(0, Number(stats.imported_likes !== undefined ? stats.imported_likes : (stats.recent_upload_likes_30d || 0)) || 0); const comments = Math.max(0, Number(stats.imported_comments !== undefined ? stats.imported_comments : (stats.recent_upload_comments_30d || 0)) || 0); const shares = Math.max(0, Number(stats.imported_shares !== undefined ? stats.imported_shares : (stats.recent_upload_shares_30d || 0)) || 0); const interactions = likes + comments + shares; const avgViews = videos > 0 ? ttImportedViews / videos : 0; const rate = ttImportedViews > 0 ? (interactions / ttImportedViews) * 100 : 0;if (el.ttName) el.ttName.textContent = profile.display_name || 'Connected TikTok account'; if (el.ttProfileSub) { const parts = ['Officially connected', ttWindowLabel(bundle.key)]; if (stats.truncated) parts.push('Partial sample'); else parts.push(number0.format(videos) + (videos === 1 ? ' video' : ' videos')); const capabilities = ttLastPayload && ttLastPayload.capabilities && typeof ttLastPayload.capabilities === 'object' ? ttLastPayload.capabilities : {}; parts.push(capabilities.profile_stats === true ? 'Profile stats active' : 'Profile stats optional'); el.ttProfileSub.textContent = parts.join(' • '); }if (el.ttViews) el.ttViews.textContent = number0.format(ttImportedViews); if (el.ttVideos) el.ttVideos.textContent = number0.format(videos); if (el.ttAvgViews) el.ttAvgViews.textContent = number0.format(Math.round(avgViews)); if (el.ttInteractions) el.ttInteractions.textContent = number0.format(interactions); if (el.ttInteractionRate) el.ttInteractionRate.textContent = rate.toFixed(rate >= 10 ? 1 : 2) + '%'; if (el.ttSynced) { const when = ttLastPayload.fetched_at ? new Date(ttLastPayload.fetched_at) : null; el.ttSynced.textContent = when && !Number.isNaN(when.getTime()) ? when.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}) : 'Now'; }const accountCapabilities = ttLastPayload && ttLastPayload.capabilities && typeof ttLastPayload.capabilities === 'object' ? ttLastPayload.capabilities : {}; const hasProfileStatsScope = accountCapabilities.profile_stats === true; const profileStats = [ [el.ttFollowersCard, el.ttFollowers, profile.follower_count], [el.ttProfileLikesCard, el.ttProfileLikes, profile.likes_count], [el.ttTotalVideosCard, el.ttTotalVideos, profile.video_count], [el.ttFollowingCard, el.ttFollowing, profile.following_count] ]; let availableProfileStatCount = 0; profileStats.forEach(([card, node, value]) => { const available = value !== null && value !== undefined && Number.isFinite(Number(value)); if (available) availableProfileStatCount += 1; if (card) card.hidden = !hasProfileStatsScope && !available; if (node) node.textContent = available ? number0.format(Math.max(0, Number(value))) : '—'; }); if (el.ttProfileStatsPanel) el.ttProfileStatsPanel.hidden = !hasProfileStatsScope && availableProfileStatCount === 0; if (el.ttProfileStatsUnlock) el.ttProfileStatsUnlock.hidden = hasProfileStatsScope;if (el.ttProgressState) { if (progress.has_previous) el.ttProgressState.textContent = 'Compared with last saved ' + ttWindowLabel(bundle.key).toLowerCase() + ' snapshot'; else el.ttProgressState.textContent = 'First ' + ttWindowLabel(bundle.key).toLowerCase() + ' snapshot'; } if (el.ttProgressNote) { el.ttProgressNote.textContent = progress.rolling_window ? (progress.delta_note || 'Rolling-window comparison: a negative change can occur when an older video ages out of the selected publish-date window, even while the account gains new views.') : 'Changes compare saved snapshots of the imported public-video sample. They are coaching indicators, not official TikTok growth analytics.'; } if (el.ttDeltaViews) el.ttDeltaViews.textContent = progress.has_previous ? ttSigned(progress.views_delta) : '—'; if (el.ttDeltaInteractions) el.ttDeltaInteractions.textContent = progress.has_previous ? ttSigned(progress.interactions_delta) : '—'; if (el.ttDeltaPosts) el.ttDeltaPosts.textContent = progress.has_previous ? ttSigned(progress.videos_delta) : '—';ttUpdateWindowControls(bundle.key); ttBuildAdvice(stats, progress, bundle.key); // Primary, unconditional creator-ideas render. This runs even when // event dispatch is disabled or a secondary listener is unavailable. ttRenderCreatorIdeasInline(ttLastPayload, bundle.topVideos, bundle.key);if (dispatch) { try { if (typeof window.CustomEvent === 'function') { const detail = Object.assign({}, ttLastPayload, { stats, progress, top_videos: bundle.topVideos, active_window: bundle.key }); root.__unisonicaTikTokAccountDetail = detail;// v19.5.5 sticky bridge: deliver directly when the optional // Creator Intelligence layer is already mounted. The custom // event is still emitted below for backwards compatibility and // for any third-party listeners that depend on it. try { if (window.UnisonicaCreatorIntelligence && typeof window.UnisonicaCreatorIntelligence.render === 'function') { window.UnisonicaCreatorIntelligence.render(detail); } } catch (_) {}root.dispatchEvent(new CustomEvent('unisonica:tiktok-account', { detail })); } } catch (_) {} }if (sync) ttSyncCalculatorFromAccount(); return true; }function ttRenderAccount(payload) { if (!el.ttProfile) return; const connected = Boolean(payload && payload.connected); if (el.ttAccountCard) el.ttAccountCard.hidden = !connected; el.ttProfile.hidden = !connected; if (!connected) { ttImportedViews = 0; ttLastPayload = null; root.__unisonicaTikTokAccountDetail = null; if (el.ttProfileStatsPanel) el.ttProfileStatsPanel.hidden = true; if (el.ttProfileStatsUnlock) el.ttProfileStatsUnlock.hidden = true; return; }ttLastPayload = payload; const profile = payload.profile || {};if (el.ttAvatar) { if (profile.avatar_url) { el.ttAvatar.src = profile.avatar_url; el.ttAvatar.alt = profile.display_name ? profile.display_name + ' TikTok avatar' : 'TikTok account avatar'; } else { el.ttAvatar.removeAttribute('src'); el.ttAvatar.alt = ''; } }ttRenderActiveWindow({ sync: Boolean(el.ttAutoSync && el.ttAutoSync.checked), dispatch: true }); }async function ttLoadAccount({ announce = false, fresh = false } = {}) { if (!el.ttProfile) return false; if (ttAccountLoadInFlight) return false;ttAccountLoadInFlight = true; try { ttSetBusy(true); if (announce && !ttLastPayload) ttShowAccountLoading();const response = await fetch(TT_API.account + (fresh ? '?fresh=1' : ''), { method: 'GET', credentials: 'same-origin', headers: { 'Accept': 'application/json' }, cache: 'no-store' }); const payload = await response.json().catch(() => ({}));if (!response.ok) { const message = ttApiErrorMessage(payload, response.status); ttSetStatus(message, 'error');// Temporary rate limits / TikTok outages must not destroy a snapshot // already rendered in the browser. Authentication failures may clear it. if (response.status === 401) { if (announce) ttShowSessionUnavailable(); ttRenderAccount({ connected:false }); } else if (!ttLastPayload) { ttRenderAccount({ connected:false }); } return false; }if (!payload.connected) { if (announce) { ttShowSessionUnavailable(); ttSetStatus('TikTok authorization is not available on the server. Start Connect TikTok again.', 'error'); } else { ttRenderAccount({ connected:false }); } return false; }ttRenderAccount(payload); if (announce) { ttSetStatus( ttImportedViews > 0 ? 'TikTok account connected. Statistics loaded successfully.' : 'TikTok account connected. No public-video views were returned in the imported sample.', 'info' ); } return true; } catch (_) { if (announce) { ttSetStatus('Could not reach the TikTok import service. The manual calculator and any already loaded snapshot remain usable.', 'error'); } return false; } finally { ttAccountLoadInFlight = false; ttSetBusy(false); } }async function ttConnectAccount(scopeMode = 'profile_stats') { if (!el.ttConnect && !el.ttProfileStatsButton) return; try { ttSetBusy(true); ttSetStatus( scopeMode === 'profile_stats' ? 'Preparing secure TikTok authorization with profile statistics…' : 'Preparing a fresh secure TikTok authorization…', 'info' );// Every click creates a brand-new uncached OAuth attempt. const attemptId = String(Date.now()) + '-' + Math.random().toString(36).slice(2, 12); const response = await fetch(TT_API.connect + '?_=' + encodeURIComponent(attemptId), { method: 'POST', credentials: 'same-origin', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Cache-Control': 'no-store, no-cache' }, cache: 'no-store', body: JSON.stringify({ return: ttCleanReturnURL(), attempt_id: attemptId, scope_mode: scopeMode === 'profile_stats' ? 'profile_stats' : 'core' }) }); const payload = await response.json().catch(() => ({})); if (!response.ok || !payload.authorization_url) { throw new Error(payload.message || 'TikTok connection is not configured yet.'); }// Location.replace avoids keeping a stale authorization URL in browser history. window.location.replace(payload.authorization_url); } catch (error) { ttSetStatus(error && error.message ? error.message : 'TikTok connection could not start. Manual entry is still available.', 'error'); ttSetBusy(false); } }async function ttDisconnectAccount() { let cleared = false; try { ttSetBusy(true); const response = await fetch(TT_API.disconnect, { method: 'POST', credentials: 'same-origin', headers: { 'Accept':'application/json' }, cache: 'no-store' }); cleared = response.ok; } catch (_) { cleared = false; } finally { if (cleared) { ttRenderAccount({ connected:false }); ttSetStatus('TikTok connection cleared. Manual calculator input remains available.', 'info'); } else { ttSetStatus('Could not confirm TikTok disconnection with the server. Please try again.', 'error'); } ttSetBusy(false); } }function ttHandleReturnMessage() { let handled = false; try { const url = new URL(window.location.href); const state = url.searchParams.get('uni_tt'); const error = url.searchParams.get('uni_tt_error'); const rawReason = url.searchParams.get('uni_tt_reason') || ''; let reason = rawReason; try { reason = decodeURIComponent(rawReason); } catch (_) {}if (state === 'connected') { handled = true; ttShowAccountLoading(); ttSetStatus('TikTok authorization completed. Loading your public-video sample…', 'info'); ttLoadAccount({ announce:true }); } else if (error) { handled = true; const labels = { oauth: 'TikTok authorization error', token: 'TikTok token exchange error', state: 'TikTok connection session error' }; const prefix = labels[error] || 'TikTok connection error'; ttSetStatus(prefix + (reason ? ': ' + reason : '. Manual entry is still available.'), 'error'); }if (state || error || rawReason) { handled = true; url.searchParams.delete('uni_tt'); url.searchParams.delete('uni_tt_error'); url.searchParams.delete('uni_tt_reason'); window.history.replaceState({}, document.title, url.toString()); } } catch (_) {} return handled; }function trackEvent(name, params = {}) { try { if (typeof window.gtag === 'function') window.gtag('event', name, params); } catch (_) {} }function trackFirstInteraction() { try { if (sessionStorage.getItem(TRACK_KEY)) return; sessionStorage.setItem(TRACK_KEY, '1'); trackEvent('zenith_calculator_interaction', { tool_version:ZENITH_VERSION }); } catch (_) { trackEvent('zenith_calculator_interaction', { tool_version:ZENITH_VERSION }); } }function onUserInput() { trackFirstInteraction(); runZenith(); }function normalizeNumberInput(input, fallback, min, max, integer = true) { if (!input) return; const raw = input.value.trim(); const normalized = clamp(parse(raw, fallback), min, max); input.value = String(integer ? Math.round(normalized) : normalized); runZenith(); }[el.views, el.goal, el.factor, el.qualified, el.growth].filter(Boolean).forEach(node => node.addEventListener('input', onUserInput, { passive:true })); if (el.views && el.ttAutoSync) { el.views.addEventListener('input', () => { if (el.ttAutoSync.checked) { el.ttAutoSync.checked = false; ttSetStatus('Manual Monthly views enabled. Account auto-sync is now off until you re-enable it.', 'info'); } }, { passive:true }); } el.views.addEventListener('blur', () => normalizeNumberInput(el.views, 0, 0, 10000000000, true)); el.goal.addEventListener('blur', () => normalizeNumberInput(el.goal, DEFAULTS.goal, 100, 1000000, true)); [el.tier, el.niche].filter(Boolean).forEach(node => node.addEventListener('change', onUserInput));$$('.z-preset').forEach(button => { button.addEventListener('click', () => { if (el.ttAutoSync && el.ttAutoSync.checked) { el.ttAutoSync.checked = false; ttSetStatus('Preset Monthly views enabled. Account auto-sync is now off until you re-enable it.', 'info'); } el.views.value = button.dataset.views; onUserInput(); try { el.views.focus({ preventScroll:true }); } catch (_) { el.views.focus(); } }); });if (el.form) el.form.addEventListener('submit', (event) => event.preventDefault()); if (el.reset) el.reset.addEventListener('click', resetZenith); if (el.print) el.print.addEventListener('click', printZenith); if (el.word) el.word.addEventListener('click', exportWord); if (el.csv) el.csv.addEventListener('click', exportCSV); if (el.copy) el.copy.addEventListener('click', copyReport); if (el.share) el.share.addEventListener('click', shareReport); if (el.text) el.text.addEventListener('click', exportText); if (el.nicheCompare) { el.nicheCompare.addEventListener('toggle', () => { if (el.nicheCompare.open) trackEvent('zenith_niche_compare_open', { tool_version:ZENITH_VERSION }); }); }$$('.z-tt-window-btn').forEach((button) => { button.addEventListener('click', () => { if (!ttLastPayload || !ttLastPayload.connected) return; const requested = button.dataset.window || 'all'; if (requested === ttActiveWindow) return; ttActiveWindow = requested; ttRenderActiveWindow({ sync: Boolean(el.ttAutoSync && el.ttAutoSync.checked), dispatch: true }); trackEvent('zenith_tiktok_stats_window', { window: ttActiveWindow, tool_version:ZENITH_VERSION }); }); });if (el.ttConnect) el.ttConnect.addEventListener('click', () => ttConnectAccount('profile_stats')); if (el.ttProfileStatsButton) el.ttProfileStatsButton.addEventListener('click', () => ttConnectAccount('profile_stats')); if (el.ttSyncNow) el.ttSyncNow.addEventListener('click', () => { if (el.ttAutoSync) el.ttAutoSync.checked = true; ttSyncCalculatorFromAccount(); }); if (el.ttRefresh) el.ttRefresh.addEventListener('click', () => ttLoadAccount({ announce:true, fresh:true })); if (el.ttDisconnect) el.ttDisconnect.addEventListener('click', ttDisconnectAccount); if (el.ttAutoSync) el.ttAutoSync.addEventListener('change', () => { if (el.ttAutoSync.checked && !el.ttProfile.hidden) ttSyncCalculatorFromAccount(); }); const handledTikTokReturn = ttHandleReturnMessage(); if (!handledTikTokReturn) { ttLoadAccount({ announce:false }); }restoreState(); runZenith({ skipSave:true });/* Backwards-compatible public functions from v16.x */ window.UnisonicaZenith = Object.freeze({ run: runZenith, reset: resetZenith, report: getCleanReport, exportText, exportWord, exportCSV, copy: copyReport, share: shareReport, importTikTok: () => ttLoadAccount({ announce:true }), connectTikTok: () => ttConnectAccount('profile_stats'), connectTikTokCore: () => ttConnectAccount('core'), syncTikTokToCalculator: ttSyncCalculatorFromAccount, disconnectTikTok: ttDisconnectAccount, getTikTokAccountDetail: () => root.__unisonicaTikTokAccountDetail || null, print: printZenith, compareNiches: () => renderNicheComparison(getValues()), projection: () => { const values = getValues(); const result = calculate(values); return { growthPercent: values.growthPercent, month3: result.projection3, month6: result.projection6, month12: result.projection12 }; }, version: ZENITH_VERSION, coreVersion: ZENITH_CORE_VERSION, modelVersion: ZENITH_MODEL_VERSION }); window.runZenith = runZenith; window.resetZenith = resetZenith; window.exportText = exportText; window.exportWord = exportWord; })();
TikTok Money Calculator 2026 for estimating creator earnings from monthly views, audience market and content niche
Unisonica TikTok Money Calculator 2026 helps creators compare earnings scenarios using monthly views, audience market, content niche and adjustable monetization assumptions.

Understanding Your TikTok Earnings Estimate

Two creators can generate a similar number of TikTok views and still see very different earnings. Audience location, content category, monetization eligibility and account performance can all influence the final result.

The TikTok Money Calculator 2026 on Unisonica is designed as a scenario-planning tool rather than an official TikTok payout quote. The lower, base and upper estimates are intended to help you compare possible outcomes without treating a single figure as guaranteed revenue.

Audience market

Viewer location can affect monetization

Advertising demand and monetization conditions vary between markets. Select the audience tier that most closely represents where the majority of your viewers are located.

Content niche

Creator categories can perform differently

Advertiser demand and commercial intent vary by topic. Use the niche comparison as a planning reference rather than assuming every creator in one category earns the same RPM.

Qualified views

Total views may differ from monetizable views

If only part of your monthly traffic is eligible for monetization, adjust the qualified-view percentage to model a more conservative earnings scenario.

Practical tip: compare your calculator scenario with your own creator analytics every month. Your historical performance is usually more useful for future planning than relying on a universal RPM number.

Learn More About TikTok RPM in 2026

RPM is commonly used to understand revenue relative to every 1,000 monetized or qualified views. Because creator results can vary substantially, benchmark ranges are best used for comparison and scenario planning rather than as guaranteed payout rates.

Our dedicated guide explores illustrative TikTok RPM ranges, audience-market differences and content-niche considerations in more detail.

Read the TikTok RPM 2026 guide →
For official platform information, see the TikTok Creator Portal and TikTok for Business.