If you sell a TikTok data API or a product built on top of one, your cost structure looks very different from a typical SaaS. Every customer triggers external API calls, database writes, worker time, and sometimes LLM inference. Some tenants are quiet. Others hammer your infrastructure at machine speed. The blended margin on your P&L hides both shapes, and by the time finance notices, the most credit-hungry 5% of tenants are already eating 60% of your variable spend.
This guide walks through how to build a working cost reporting system for a multi-tenant TikTok SaaS: the unit economic model, a daily Python ETL, fair allocation of shared infrastructure, dashboards your sales team will actually open, and the monthly review ritual that connects engineering, sales, and finance. The same patterns work whether you resell a provider like TikLiveAPI or run your own scraping stack.
Before you can report on cost, you need a clean per-tenant cost equation. For a TikTok SaaS, the variables usually look like this.
X-Api-Key against an external provider, this is the line item that scales most aggressively with usage.play, hdplay, wmplay URLs proxied through your CDN), and CSV exports.The simplest workable equation for a single tenant t in month m is:
cost(t, m) =
credits(t, m) * unit_credit_cost
+ db_writes(t, m) * unit_write_cost
+ db_reads(t, m) * unit_read_cost
+ worker_seconds(t, m) * unit_worker_cost
+ llm_tokens(t, m) * unit_llm_cost
+ egress_gb(t, m) * unit_egress_cost
+ shared_infra_pool(m) * (api_calls(t, m) / total_api_calls(m))
The last term is the only "fair" way to split shared infrastructure that does not have a clean per-tenant meter. We use API call share because, in a TikTok scraping workload, almost every fixed cost scales with request volume.
You cannot report on what you do not measure. The minimum events you need to capture per request:
tenant_id (resolved from your auth header, in our case X-Api-Key).endpoint (for example /userid/, /userinfo-by-username/, /post-detail/).credits_charged.upstream_latency_ms and worker_ms.response_bytes.llm_input_tokens, llm_output_tokens (if relevant).http_status.Endpoint matters more than people expect. A call to /post-detail/ that returns play, hdplay, and wmplay URLs is much cheaper than /post-comments/ with deep pagination, which can fan out into hundreds of upstream calls per logical "fetch all comments" operation. Without endpoint-level breakdown, you cannot tell sales which use case is destroying your margin.
If you are reselling, your upstream provider's billing data is your ground truth. If you are running your own infrastructure, fold the data warehouse copies of your AWS or GCP cost exports into the same fact table.
The goal is a flat tenant_cost_daily table that finance and engineering both query. Below is a compact ETL skeleton that pulls request logs, joins price points, allocates shared infrastructure, and writes the daily fact.
import pandas as pd
from sqlalchemy import create_engine
from datetime import date, timedelta
PRICES = {
"credit": 0.0008,
"db_write": 0.0000012,
"db_read": 0.0000004,
"worker_sec": 0.000018,
"llm_input_token": 0.000003,
"llm_output_token": 0.000015,
"egress_gb": 0.085,
}
def build_daily_cost(engine, target_day: date) -> pd.DataFrame:
requests = pd.read_sql(
"""
SELECT tenant_id,
endpoint,
credits_charged,
db_writes,
db_reads,
worker_ms,
llm_input_tokens,
llm_output_tokens,
response_bytes
FROM request_log
WHERE event_date = %(d)s
""",
engine,
params={"d": target_day},
)
g = requests.groupby("tenant_id", as_index=False).agg(
api_calls=("endpoint", "count"),
credits=("credits_charged", "sum"),
db_writes=("db_writes", "sum"),
db_reads=("db_reads", "sum"),
worker_sec=("worker_ms", lambda s: s.sum() / 1000.0),
llm_in=("llm_input_tokens", "sum"),
llm_out=("llm_output_tokens", "sum"),
egress_gb=("response_bytes", lambda s: s.sum() / 1e9),
)
g["variable_cost"] = (
g["credits"] * PRICES["credit"]
+ g["db_writes"] * PRICES["db_write"]
+ g["db_reads"] * PRICES["db_read"]
+ g["worker_sec"] * PRICES["worker_sec"]
+ g["llm_in"] * PRICES["llm_input_token"]
+ g["llm_out"] * PRICES["llm_output_token"]
+ g["egress_gb"] * PRICES["egress_gb"]
)
shared_pool = pd.read_sql(
"SELECT total_usd FROM shared_infra_daily WHERE event_date = %(d)s",
engine, params={"d": target_day},
).iloc[0]["total_usd"]
total_calls = g["api_calls"].sum() or 1
g["allocated_shared"] = shared_pool * (g["api_calls"] / total_calls)
g["total_cost"] = g["variable_cost"] + g["allocated_shared"]
g["event_date"] = target_day
return g
if __name__ == "__main__":
eng = create_engine("postgresql://etl@warehouse/analytics")
df = build_daily_cost(eng, date.today() - timedelta(days=1))
df.to_sql("tenant_cost_daily", eng, if_exists="append", index=False)
Three details worth calling out:
price_version column.Three allocation methods come up in every cost meeting. Pick one explicitly, write it down, and defend it.
Divide shared cost by tenant count. Almost always wrong: a 50-call-per-day hobby account absorbs the same overhead as a 5-million-call automation pipeline.
Allocate shared cost in proportion to MRR. Easy to compute and defensible to finance, but it makes loss-making heavy users look healthier than they are.
Allocate by API call count, worker seconds, or a blended utilization score. This matches engineering reality: if a tenant doubles their request volume, the share of shared cost they cause also roughly doubles. It also exposes unprofitable tenants honestly, which is the whole point of the report.
Once tenant_cost_daily is populated, join it to your billing table to produce the dashboard finance wants:
SELECT
t.tenant_id,
t.plan,
SUM(r.mrr_usd) AS mrr,
SUM(c.total_cost) AS cost_30d,
SUM(r.mrr_usd) - SUM(c.total_cost) AS gross_profit_30d,
CASE
WHEN SUM(r.mrr_usd) = 0 THEN NULL
ELSE 1 - (SUM(c.total_cost) / SUM(r.mrr_usd))
END AS gross_margin
FROM tenants t
LEFT JOIN tenant_cost_daily c
ON c.tenant_id = t.tenant_id
AND c.event_date >= CURRENT_DATE - INTERVAL '30 days'
LEFT JOIN tenant_revenue_daily r
ON r.tenant_id = t.tenant_id
AND r.event_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY t.tenant_id, t.plan
ORDER BY gross_profit_30d ASC;
Recommended panels for the dashboard:
/post-comments/ dominates spend.In every multi-tenant TikTok SaaS we have looked at, a Pareto pattern shows up almost immediately: the top 5% of tenants drive 50% to 70% of total credit spend. The shape is structural, not a bug, and you should design around it.
To surface this group, rank tenants by 30-day credit consumption and compute the cumulative share:
WITH ranked AS (
SELECT tenant_id,
SUM(credits) AS credits_30d,
SUM(SUM(credits)) OVER () AS total_credits,
ROW_NUMBER() OVER (ORDER BY SUM(credits) DESC) AS rk
FROM tenant_cost_daily
WHERE event_date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY tenant_id
)
SELECT tenant_id,
credits_30d,
credits_30d / total_credits AS share,
SUM(credits_30d) OVER (ORDER BY rk) / total_credits AS cumulative_share
FROM ranked
ORDER BY rk;
For each tenant in the top decile, finance and engineering should know: their endpoint mix, whether they are on the right plan, and what their actual gross margin is. These accounts deserve named account management, not generic email automation.
The cost report becomes operational the moment you alert on variance. Two rules work well in practice:
/post-comments/" scenario before the monthly review.When the alert fires, the on-call investigation checklist is small: which tenant, which endpoint, has their plan changed, is the upstream provider's status normal, and is there a code regression. Document the root cause in the same ticket each time so the next variance investigation runs faster.
The dashboard is leverage. The monthly review is where it converts to decisions. A 45-minute agenda we have seen work:
The single biggest cultural change a cost report unlocks is sales finally seeing the unit economics of the deals they close. Two practices help.
First, build a "deal margin estimator" into your CRM. When a prospect is sized, sales picks the expected endpoints and call volume, and the estimator returns a forecasted monthly cost using the same price book the ETL uses. If a deal is signed at a margin under your threshold, it requires sign-off from finance. This is a cultural change, not a technical one, and it pays back fast.
Second, share the dashboard. Read-only access for AEs and CSMs, with a focused view that hides PII but shows their book of business by cost and margin. The first time a CSM sees their largest account is unprofitable, the conversation about upsell happens on its own.
The headline KPI we recommend is straightforward:
cost_per_customer_30d = SUM(total_cost) / COUNT(DISTINCT active_tenant_id)
Segment it by plan tier and by cohort month. The plan-tier cut tells you whether your pricing page is correctly graduating users. The cohort cut tells you whether newer cohorts are heavier or lighter than older ones (heavier almost always means a new use case is emerging and pricing has not caught up).
Pair it with two supporting KPIs: gross margin by plan and credits per dollar of revenue. Together these three numbers answer almost every question finance and the board will ask.
Concrete thresholds, calibrated by the teams we have worked with:
Publish the thresholds internally. The point is not to be punitive; it is to make sure no account silently destroys margin for six months while nobody notices.
An unprofitable tenant is not automatically a bad tenant. Most of them are signaling that they have outgrown their plan. Useful upsell motions:
/post-comments/ or LLM-summarized output), price that endpoint separately.If you want to test these paths against a live API before pricing them, the playground and documentation are the fastest way to validate which endpoints a customer actually needs. For commercial conversations, contact us.
Daily is the right cadence. Hourly is overkill for finance and adds operational noise. Weekly is too slow to catch a runaway tenant before they cost you several hundred dollars.
Yes. Without revenue context, every cost looks bad and every optimization looks urgent. With revenue context, engineering can prioritize efficiency work on the endpoints that move margin, not just the ones that look expensive in isolation.
Reconstruct it. Take the monthly invoice, divide by the call count from your own logs, and use the implied unit cost in your ETL. Reconcile monthly. Any provider worth using will expose X-Api-Key-scoped usage and let you tie usage back to spend.
Track them separately. Their cost is a CAC line item, not a COGS line item. Fold them into the headline cost-per-customer KPI only after they convert.
70% to 80% blended is realistic if you resell upstream and add value through caching, dashboards, and integrations. Below 60% blended, your pricing or your plan structure needs work. You can model your own break-even against the published rates on the pricing page.
No. Postgres works fine up to a few hundred million request log rows with good partitioning. Move to BigQuery, Snowflake, or ClickHouse when ETL runtime starts exceeding the daily window, not before. Premature warehouse adoption is one of the most common ways early-stage SaaS teams burn six months they did not have.
Wire your variance alerts to also check the upstream status page. A credit spike that coincides with elevated upstream latency is almost always retry-storm-driven, not a customer behavior change. Other useful posts on related topics are on the blog.
Ready to put what you read into code? Try our endpoints live or grab the full reference.