One post connects to distinct public and owner report cards.
Match the source scope before comparing counts.

A reading is one value from one source at one time. Reconcile means you record what matches, what differs in scope, and what stays unknown. Scope is what a number includes. Scope covers the metric, the posts, and the time window. A window is the start and end of a period.

A public snapshot cannot rebuild a missing monthly account report. A snapshot is one read of public counters at one time. Keep a scope difference you can explain apart from a difference you cannot explain.

Comparison steps

1. State the reporting need.

A public post ledger can document known posts you can read now. A ledger is one row for each post. It cannot fill every private report column. It cannot create an earlier month you did not save.

Write your need in one line. If you need a full account report for a past month, stop. Say that a public snapshot cannot supply that report. If you already hold an export, you can continue. That export must refer to known public posts.

2. Read one known public post first.

An Actor is a hosted data collection program. A Task is a saved input preset for that Actor. The X Post Metrics Actor collects public rows. You do the comparison. The Actor does not assign a result class. A saved Task is a starting input. It is not proof that the comparison is automatic.

Build 1.0.4 is the public version used for these field rules. That build accepts `startUrls` with 1 to 1,000 known public status URLs. A status URL is the web address of one post. Here, a post is one public X message with its own status URL. X is the public site at x.com.

Check a run in this order. Start with the saved Task. Replace the sample with your pilot URL. Then read the native row fields. Then read `postMetricStatus` for each metric. Then read the run summary. Keep the run summary off the post row.

The current saved Task input is a sample:

json
{
  "startUrls": [
    "https://x.com/OpenAI/status/2074907025537224840",
    "https://x.com/OpenAI/status/2074907027378511884",
    "https://x.com/OpenAI/status/2074907031862190406"
  ]
}
Comparison steps

Those three URLs are samples. Replace them with public posts you know. A pilot is your first small run. Use one URL for the pilot. One URL keeps the test small.

This Actor does not search a profile timeline. It does not build a date-range account report. It does not open private analytics. It does not recover history you did not save. It does not collect protected posts. A protected post is not public. It does not use account credentials. Credentials are passwords or other login secrets.

Each input URL produces one attempted row. The row includes `status` and `statusReason` even when useful facts are missing. `status` says how that row ended. `statusReason` gives the short reason. A missing counter is not zero. Zero is a returned number. Null means the value was not available.

Check four things on the pilot row before you trust it. Check that `postId` and `postUrl` match the post you asked for. Check `status`. Check `observedAt`. Check `postMetricStatus` for each metric you will use. `observedAt` is the observation time. Observation time is when the counter was read. Publication time is when the post was published. Do not swap those times.

The native row fields are `inputUrl`, `postUrl`, `postId`, `profileId`, `views`, `likes`, `replies`, `reposts`, `quotes`, `bookmarks`, `profileFollowers`, `postMetricStatus`, `status`, `statusReason`, and `observedAt`.

`inputUrl` is the URL you sent. `postUrl` is the post URL on the row. `postId` is the post ID. `profileId` is the account ID. A counter is a number stored for that post or account. `views` counts public view events. The row also has `likes`, `replies`, `reposts`, `quotes`, and `bookmarks`. A like is a mark that someone liked the post. A reply is a response. A repost is a share of the post. A quote is a share that adds words. A bookmark is a saved post. Do not add these counters together here. This method does not make a rate from them.

`profileFollowers` is the public follower count for that account at `observedAt`. A follower is an account that follows the profile. Do not use `profileFollowers` as a post view count.

Precision says if a number is exact, rounded, or unknown. `postMetricStatus` gives that precision for each returned metric. Keep the precision with the number. If a metric is missing, leave it missing. Do not write 0 to fill the gap.

A control is a checked public run used to see which fields come back. A current public control returned the requested identity and time fields. Those fields include `postId`, `postUrl`, `profileId`, publication time, and `observedAt`. It also returned `views`, `likes`, `replies`, `reposts`, `quotes`, `bookmarks`, and `profileFollowers`. Its `status` was ok.

Each returned `postMetricStatus` metric was present/exact/public_post.

Comparison steps
Returned metricControl postMetricStatus
viewspresent/exact/public_post
likespresent/exact/public_post
repliespresent/exact/public_post
repostspresent/exact/public_post
quotespresent/exact/public_post
bookmarkspresent/exact/public_post

Present means the metric was on the row. Exact means that control's value was exact. `public_post` means the source was a public post. That control shows that public counters can be collected. It does not show that a public counter equals a private export. Your own run can differ. Read your own row. Do not copy the control status onto a post the control did not read.

The run summary is not a post row. Its fields are `inputCount`, `datasetRows`, `usefulRows`, `billableRows`, `failedRows`, `statusCounts`, and `stopReason`.

`inputCount` counts input URLs. `datasetRows` counts rows written. `usefulRows` counts rows with useful facts. `billableRows` counts rows the run marks for billing. `failedRows` counts failed rows. `statusCounts` groups row counts by status. `stopReason` says why the run stopped. Do not paste any of these into a post view cell. This article does not invent those counts. A price is not stated here.

3. Record the owned export you already hold.

An export is a file from an analytics report you are allowed to use. An authorized report is a report you are allowed to use. Use an export only when you are allowed to use it. This method does not pull a private export from X. It does not replace a dashboard for free. It does not bypass an access rule.

Older public posts do not set today's price or access for X Analytics. See Alternatives for Twitter analytics and X (Twitter) analytics. If you do not already hold the report, leave that side unknown.

If you do hold an authorized report, copy these items from that report. Copy the post ID. Copy the metric name. Copy the metric definition from the report's own notes. Copy the date range and the timezone. Copy the extraction time. Extraction time is when the report file was taken. If the report states paid or organic scope, copy that too. Paid scope covers promoted posts. Organic scope covers posts the report marks as not promoted. If the report does not state that split, do not invent it.

Do not assume a column named impressions matches public views. Do not treat an account total as one post's total. Use the report's definition. If the definition is missing, the comparison stays unknown.

4. Match rows on the post ID.

Match `postId` to the post ID in your export. Do not match on title or text. Two different posts can use the same words. A changed caption can still be the same post ID. The ID is the key.

Keep a public row that has no report row. Keep a report row that has no public row. Keep an ID you cannot read as unknown. Do not turn a missing match into 0.

Some report rows are account-period rows. An account-period row covers many posts for one window. Do not join that row to one post. Do not treat the account number as that post's number. Write the aggregation level on the row. Aggregation level says whether the row is one post, one day, or one account.

5. Map scope before you compare numbers.

Give the pair one scope label.

`same_metric_scope` means the metric definition, the post, and the time scope agree.

`different_metric_scope` means you can show that the definition or the period differs.

`definition_unknown` means a definition you need is missing.

A public cumulative counter says what the total is at the read. Cumulative means events add from publication until `observedAt`. A selected-period report says what fell inside one window. A window that ends yesterday can differ from a total read today. Those two numbers answer different questions.

Public views are view events. X says they can include repeated views and the author's own view. They are not unique people. Read the current view count help before you use `views`. That page does not make `views` equal to every export metric. If your export uses another definition, use the export's words. If you cannot read the export definition, use `definition_unknown`.

Updates can lag. Lag means the later read does not include a recent event yet. Do not name lag as the cause unless you have evidence. A guess is not evidence.

6. Compute a difference only when the comparison allows it.

If the definition and the period align enough, you may compute the displayed difference. Use plain subtraction.

text
displayed difference = public value - owned value
observed counter delta = later saved public value - earlier saved public value
Comparison steps

Keep the precision beside the result. If `postMetricStatus` is not exact, do not call the result exact. If the public value is rounded, an exact match may be impossible. If either value is missing, do not compute the difference.

Two saved public snapshots can show a change between those saved times only. An observed counter delta is that change. It exists only for times you saved. Do not invent an earlier baseline. A baseline is a starting read. You cannot recover a missing month by guessing a start value.

A counter can go down when X corrects it. A negative change can be real. Do not call every drop a bad file read.

If scope does not align, you may still show the subtraction as arithmetic. Label it as arithmetic. Do not label it as the cause.

7. Classify the result.

Use one result class for the comparison you can support.

`explained_scope_difference` means you can point to a known scope difference. This class does not explain the full size of a numeric gap.

`timing_or_precision_limit` means the times differ, or precision blocks an exact match. Use it only when that limit is what you can show.

`comparable_values` means the post, the definition, and the time scope align enough to compare the two numbers. A match on one row does not prove the whole report is complete.

`unresolved_difference` means you cannot support a reason. Leave the reason unknown.

Fraud would mean the numbers were faked. A gap does not prove fraud. It does not prove that an extra number was paid. It does not prove that the analytics tool is broken. It does not prove that X hid the post. Keep a reason only when both receipts support it. A receipt is the public row or the owned report row you hold. If the definition is missing, the reason stays unknown.

8. Deliver the post-level table and a short note.

Use one row per post. Show the post ID and both sources. Show publication time and observation time in separate cells. Show the metric name, the definition, the window, and the precision. Show a missing value as missing. Show the scope label and the result class. If a numeric gap stays unexplained, say that in the note.

If you have only public rows, report those rows and the limits above. Do not claim monthly impressions. Do not claim unique reach. Do not claim a private engagement total. Do not claim campaign return. Do not turn the counters into a rate.

Close the note with one metric on one post. That row is either comparable or unresolved. Do not turn an unknown gap into a cause.

Fictional post ledger

The five posts below are original fiction. They are not a product result. They are not rows from the Actor. In this fiction, no dashboard was replaced. No private export was retrieved. Publication time is not given. Do not copy an observation time into the publication-time cell. Do not turn a view count into unique people.

P1 through P5 are fictional post IDs. None of these posts is a real export.

Fictional post ledger
PostPublication timePublic metricPublic valuePublic observation timePublic definitionPublic windowPublic precisionOwned metricOwned valueOwned timeOwned definitionOwned windowScopeArithmeticResult
P1not givenviews1,500October 13 10:00 UTCpublic view eventscumulative through the observation timenot statedimpressions1,200not givendocumented impressions for the stated windowOctober 1–12; timezone not givendifferent_metric_scope1,500 - 1,200 = 300explained_scope_difference for scope only; the 300 gap is not fully explained
P2not givenviews1,000 then 1,200October 13 10:00 UTC and October 13 12:00 UTCexact cumulative public viewscumulative through each saved readexactno owned rowno owned rowno owned rowno owned rowno owned rowsame public views definition at two saved times1,200 - 1,000 = 200observed counter delta for those two saved times only
P3not givenviewsnull, not 0not givenpublic views were not availablenot availablenot applicablenot given500not givennot givennot givenpublic value missing, so scope is not mappeddo not calculateunavailable public comparison; not a gap of -500
P4not givennot given2,000not givennot givennot givenunknownnot givennot givennot givendefinition missingnot givendefinition_unknowndo not calculateunresolved_difference
P5not givendocumented counter250same time as the owned reading; exact clock time not givensame documented counter as the owned sidesame window as the owned side; dates not givennot stated beyond 250documented counter250same time as the public reading; exact clock time not givensame documented counter scopesame window as the public side; dates not givensame_metric_scope250 - 250 = 0comparable_values for this counter only

UTC is the clock used for P1 and P2. It does not follow local daylight time.

P1 is a known scope difference. The public cell is cumulative views read on October 13 at 10:00 UTC. The owned cell is impressions for October 1–12. Views and window impressions are not the same metric. A total through October 13 and a window that stops on October 12 are not the same period. That is why the scope label is `different_metric_scope`. The result class `explained_scope_difference` covers that scope fact only.

The subtraction 1,500 - 1,200 = 300 is arithmetic. It is not 300 verified missing impressions. It is not proof of a collection error. You cannot split the 300 from these two receipts. You do not know how many views fell on October 13. You do not know how many were repeats or the author's own views. You do not know how the impression rule differs inside that 300. Leave that part unexplained.

P2 has two exact public reads of cumulative views. At October 13 10:00 UTC the counter shows 1,000. At October 13 12:00 UTC it shows 1,200. The observed counter delta is 1,200 - 1,000 = 200. That is the displayed change between two saved times. It is not 200 unique people. It is not a recovered monthly report. There is no owned row for P2 in this fiction. Do not invent a start value before 10:00. Do not describe the path between the two hours. You did not save those in-between reads.

P3 has public views of null and a report value of 500. Null means the public view counter was not available. It does not mean zero views. Because the public value is missing, you do not subtract. 500 - 0 is the wrong equation. 0 - 500 is also wrong. There is no gap of -500. The report value stays on the report side. It is not a public view total. This result is an unavailable public comparison. It is not a finished mismatch class.

P4 shows a public value of 2,000 with unknown precision. The report has no definition. The metric names, times, and windows are not given. The scope label is `definition_unknown`. You cannot support a cause. The result is `unresolved_difference`. Do not guess rounding. Do not guess lag. Do not guess a second metric name.

P5 uses the same documented counter, the same scope, and the same time on both sides. Both values are 250. The displayed difference is 250 - 250 = 0. The scope label is `same_metric_scope`. The result is `comparable_values`. That result covers this one counter only. It does not show that every post in a report was checked. It does not show that every other metric on P5 matches. It does not show that the report is complete.

Sources & further reading

By Chronometrica · Updated

Back to top ↑