Calculate Honest X Engagement Rates
Use the Desearch API to calculate an X account's honest engagement rate.
Introduction
30 Second Summary
A big view count can make an X account look more engaging than it really is. Reposts or duplicate results can quietly inflate the percentage.
In this project, you will pull recent posts from one X account through the Desearch API. Your Python script will compare a naive engagement rate with an honest rate that excludes misleading data.
What You'll Build
You'll run your analysis to see a per-post table followed by an account summary that places the naive rate beside the honest rate.
By the end of this project, you'll have:
- A per-post table that lets you see which recent posts qualify for per-view maths.
- A cleaned account summary where reposts or repeated posts no longer inflate the calculation.
- A side-by-side rate comparison that shows exactly how much the naive calculation overstated engagement.
- Secret Mission: An optional challenge to push your skills further.
Are there any prerequisites?
You'll need Python available on your computer. Step 1 helps you get a Desearch key before you fetch any posts.
Before We Start
An honest engagement rate starts with dependable inputs. Your analysis cannot run until your terminal can reach the tools it needs.
This step prepares Python plus curl for the later calculations. You will also choose the X account that anchors every comparison.
In this step, get ready to:
- Confirm that your Python output starts with Python 3.
- Confirm that curl reports its version.
- Select one public X account handle for the analysis.
Check Python in your terminal
Python transforms the later API response into a per-post table. A version check proves your terminal can reach the interpreter.
macOS
- Press Cmd+Space to open Spotlight.
- Type Terminal into the search field.
- Press Return to open Terminal.
Windows
- Press the Windows key to open search.
- Type Windows PowerShell into the search field.
- Select Windows PowerShell from the results.
- Display the interpreter version with the command for your operating system:
macOS
python3 --version
What Does This Command Do?
The python3 command selects the Python interpreter. The --version option prints its installed version.
Windows
python --version
What Does This Command Do?
The python command selects the default Python interpreter. The --version option prints its installed version.
✔️ Python reports its version
Good start. Your terminal can reach the Python interpreter that performs the later calculations.
ⓧ Python is missing or reports another major version
Your terminal cannot reach the expected interpreter yet.
The device password prompt only grants the official installer permission to add Python.
- Download Python from the official Python downloads page.
- Run the installer you downloaded.
- Complete the installer with its default options.
- Approve the operating system permission request with your device credentials.
- Close the current terminal window.
- Open a fresh terminal using the platform steps above.
- Repeat the Python version check for your operating system.
The new output should start with Python 3.
Still Missing Python?
- Restart your computer if a fresh terminal still cannot find Python.
- Run the installer again if the installation stopped before its completion screen.
Help me troubleshoot why my terminal cannot find Python after installation.
Check curl in your terminal
The curl command sends HTTP requests from your terminal. Its version output proves the request tool is available for the fetch step.
- Display the installed curl version with the command for your operating system:
macOS
curl --version
What Does This Command Do?
The curl command starts the request client. The --version option prints its version plus supported features.
Windows
curl --version
What Does This Command Do?
The curl command starts the request client. The --version option prints its version plus supported features.
✔️ curl reports its version
Your request tool is ready. The terminal can reach curl for the account fetch.
ⓧ The curl command is missing
Your terminal does not currently expose the curl command. The curl download page lists packages for each operating system.
- Open the official curl download page.
- Choose the tool package listed for your operating system.
- Follow the package provider's installation instructions.
- Close the current terminal window.
- Open a fresh terminal using the platform steps above.
- Repeat the curl version check.
The new output should begin with curl.
Still Missing curl?
- Restart your computer if the package installed successfully but the fresh terminal cannot find curl.
- Confirm that you chose a tool package for your operating system.
Help me troubleshoot why curl is unavailable after installation.
Choose your target account
A stable comparison needs one account throughout the project. Using one target prevents later numbers from mixing posts from different profiles.
- Visit the profile of one public X account with recent posts.
- Confirm that the account's recent posts are visible on its profile.
- Record the account handle here: your target X account handle.
Before you run the final check, do you expect both tools to print version details?
- Verify both tools from the same terminal with the commands for your operating system:
macOS
python3 --version
curl --version
What Does This Check Prove?
- The python3 result confirms that your calculation tool is available.
- The curl result confirms that your request tool is available.
Windows
python --version
curl --version
What Does This Check Prove?
- The python result confirms that your calculation tool is available.
- The curl result confirms that your request tool is available.
The first line begins with Python 3. The next output block begins with curl.
That is the setup locked in. Your terminal is ready for the first real request.
Missing One of the Version Outputs?
- Return to the matching outcome tab above for the missing tool.
- Open a fresh terminal after completing any installation.
- Run the final check again from the fresh terminal.
Help me diagnose which setup check is failing.
Your tools plus target account are ready. Next, you will get a Desearch key and fetch the account's recent posts.
Fetch Recent Posts
Reliable engagement maths starts with a fixed set of source posts from the selected X account. The Desearch API returns that data as JSON through an authenticated request.
You will create a Desearch key for one request. The saved response becomes the shared input for every later calculation.
In this step, get ready to:
- Create a Desearch API key in Desearch Console.
- Keep the key in an environment variable for the current terminal session.
- Fetch recent posts into desearch_posts.json.
Create a Desearch API key
An API key identifies your Desearch account when a request reaches the API. Anyone holding the key can make requests through your account.
This step exposes the credential once, so take your time while copying it. You will keep it out of every project file.
- Open the Desearch Console in your browser.
- Sign up or sign in to reach Desearch Console.
- Select API Keys in Desearch Console.
- Click Generate API Key.
- Enter x-engagement-local as the key name.
- Copy the generated key when its full value appears.
- Close the key reveal after copying the value.
- Return to API Keys to confirm that x-engagement-local is listed.
Your named key is now ready for local requests. Its full value stays on your clipboard until you place it in the terminal session.
Store the key in your terminal
An environment variable lets commands read a value from the current terminal session. Closing that terminal removes the value from its environment.
- Switch back to the terminal from earlier.
- Replace your-desearch-api-key-here in your platform command with the complete key from your clipboard.
macOS
- Limit the key to this terminal session by running this command:
export DESEARCH_API_KEY="your-desearch-api-key-here"
What does this command do?
The command stores your key under DESEARCH_API_KEY in the current shell. The upcoming request reads the credential without placing it inside desearch_posts.json.
- Keep this terminal session open for the authenticated request.
Windows
- Limit the key to this PowerShell session by running this command:
$Env:DESEARCH_API_KEY = "your-desearch-api-key-here"
What does this command do?
The $Env: prefix stores your key under DESEARCH_API_KEY for the current PowerShell session. The upcoming request reads the credential without placing it inside desearch_posts.json.
- Keep this PowerShell session open for the authenticated request.
Your current terminal can now authenticate with Desearch. The request itself provides a safe check that the variable contains a working key.
Fetch and verify the recent posts
The documented posts endpoint accepts the account username through its request URL. Use the selected handle without its leading @ symbol.
- Enter the selected handle here: selected X account handle without @.
This request contributes to your Desearch usage. One saved response is enough for the remaining calculations in this project.
Before you run the request, do you expect the environment variable to authenticate it without exposing the key in the URL?
macOS
- Save one authenticated response to your Desktop by running this command:
curl --fail-with-body --request GET \
"https://api.desearch.ai/twitter/user/posts?username=[[X_HANDLE="selected X account handle without @"]]" \
--header "Authorization: $DESEARCH_API_KEY" \
--header "Content-Type: application/json" \
--output ~/Desktop/desearch_posts.json
What does this request do?
- The /twitter/user/posts endpoint retrieves recent posts for the username in the request URL.
- The Authorization header reads the key from DESEARCH_API_KEY.
- The --output option saves the raw response as desearch_posts.json on your Desktop.
- Keep the terminal open after the command returns to its prompt.
Windows
- Save one authenticated response to your Desktop by running this command:
curl.exe --fail-with-body --request GET `
"https://api.desearch.ai/twitter/user/posts?username=[[X_HANDLE="selected X account handle without @"]]" `
--header "Authorization: $Env:DESEARCH_API_KEY" `
--header "Content-Type: application/json" `
--output "$HOME\Desktop\desearch_posts.json"
What does this request do?
- The /twitter/user/posts endpoint retrieves recent posts for the username in the request URL.
- The Authorization header reads the key from DESEARCH_API_KEY.
- The --output option saves the raw response as desearch_posts.json on your Desktop.
- Keep the PowerShell session open after the command returns to its prompt.
That is the data pull complete. Your account analysis now has one fixed source response that you can inspect without making another request.
Before you inspect the file, which post details do you expect the response to contain?
macOS
- Validate the saved response by running this command:
python3 -m json.tool ~/Desktop/desearch_posts.json
What does this check prove?
The JSON tool parses the complete file before printing an indented version. Structured output confirms that desearch_posts.json contains valid JSON.
Windows
- Validate the saved response by running this command:
python -m json.tool "$HOME\Desktop\desearch_posts.json"
What does this check prove?
The JSON tool parses the complete file before printing an indented version. Structured output confirms that desearch_posts.json contains valid JSON.
You should see an indented JSON response containing recent post records. This confirms that the saved file is readable by Python.
Inspect the response for post identifiers plus post text. Look for repost metadata plus engagement metrics. View-count values appear when available. Pagination data can also appear when another page is available.
Did the response fail?
If the file contains an authentication error, repeat the environment variable command in the same terminal session. Make sure the complete key replaced the placeholder.
If the response says the account is unavailable, confirm that the handle contains no @ symbol. Confirm that the selected account is public.
If Python cannot parse the file, check the curl output for a failed request before trying again.
Ask for help with a redacted response. Help me diagnose my Desearch request without sharing my API key.
Your raw post dataset is saved for repeatable analysis. Next, you will calculate the naive engagement rate that the later filters need to challenge.
Calculate the Naive Rate
The raw response from your Desearch API request now holds the evidence for your engagement audit. Each returned post includes the metrics needed for a first calculation.
Those post-level numbers still need a single baseline. This step deliberately counts every returned post so you can measure how the first-pass rate behaves before the audit gets stricter.
In this step, get ready to:
- Load every returned post from desearch_posts.json.
- Calculate one naive per-view engagement rate from all available totals.
- Print the baseline rate with its post count.
Build the naive audit
The baseline adds likes, reposts, replies, quotes, and bookmarks for each post. It divides the combined engagements by the available view total.
- Create engagement_audit.py beside desearch_posts.json by pasting the code below into a new file in your code editor:
import json
# Load the saved API response for an offline calculation.
with open("desearch_posts.json", encoding="utf-8") as response_file:
data = json.load(response_file)
# Preserve fields that later audit passes need.
posts = data.get("tweets", [])
audit_rows = []
engagement_fields = (
"like_count", "retweet_count", "reply_count",
"quote_count", "bookmark_count",
)
for post in posts:
engagements = sum(post.get(field, 0) or 0 for field in engagement_fields)
audit_rows.append({
"post_id": str(post.get("id", "")),
"is_repost": bool(post.get("is_retweet") or post.get("retweet")),
"engagements": engagements,
"views": post.get("view_count"),
})
# Pool every returned post into the deliberately naive baseline.
total_engagements = sum(row["engagements"] for row in audit_rows)
total_views = sum(row["views"] or 0 for row in audit_rows)
naive_rate = 100 * total_engagements / total_views if total_views else 0.0
print(f"Naive posts: {len(audit_rows)}")
print(f"Naive engagement rate: {naive_rate:.3f}%")
How does the baseline work?
- The engagement_fields tuple defines the five actions included in engagement.
- Each entry in audit_rows keeps the post identifier. It also keeps the repost flag plus the view count.
- The total_engagements value pools engagement actions from every returned post.
- The total_views value pools every available view count.
- The naive_rate value converts the combined ratio into a percentage.
- Save engagement_audit.py.
Use this comparison to confirm that every part of the baseline is in place.
✔️ Awesome, I've got everything!
Your saved engagement_audit.py now contains the raw-data loader, per-post audit rows, and baseline calculation.
ⓧ I'd like to double check the full code
import json
# Load the saved API response for an offline calculation.
with open("desearch_posts.json", encoding="utf-8") as response_file:
data = json.load(response_file)
# Preserve fields that later audit passes need.
posts = data.get("tweets", [])
audit_rows = []
engagement_fields = (
"like_count", "retweet_count", "reply_count",
"quote_count", "bookmark_count",
)
for post in posts:
engagements = sum(post.get(field, 0) or 0 for field in engagement_fields)
audit_rows.append({
"post_id": str(post.get("id", "")),
"is_repost": bool(post.get("is_retweet") or post.get("retweet")),
"engagements": engagements,
"views": post.get("view_count"),
})
# Pool every returned post into the deliberately naive baseline.
total_engagements = sum(row["engagements"] for row in audit_rows)
total_views = sum(row["views"] or 0 for row in audit_rows)
naive_rate = 100 * total_engagements / total_views if total_views else 0.0
print(f"Naive posts: {len(audit_rows)}")
print(f"Naive engagement rate: {naive_rate:.3f}%")
Run the baseline audit
The naive result becomes the benchmark for every later correction. Keep the printed rate plus its post count so you can measure each change.
Before you run the script, ask yourself whether the post count will match every item returned in tweets.
- Run the baseline audit from the folder containing both files by using the command for your system:
macOS
python3 engagement_audit.py
Windows
python engagement_audit.py
You'll see one line reporting the naive post count. The next line reports the naive engagement rate as a percentage.
That baseline is now locked in. You have a concrete number to challenge as the audit gets stricter.
- Record the printed rate here: your naive engagement rate.
- Record the printed post count here: your naive post count.
Script not printing two lines?
Confirm that engagement_audit.py sits in the same folder as desearch_posts.json.
If the post count is zero, confirm that the saved response contains a top-level tweets list.
Ask for help with the script output.
Your first-pass engagement rate is recorded. Next, you'll test whether every returned post belongs in that calculation.
Exclude Reposts
You now have a baseline that counts every returned post. That naive rate can include engagement numbers attached to reposted content.
This step uses the Desearch API repost metadata to separate those rows. You will retain the naive rate so the effect of the filter stays visible.
In this step, get ready to:
- Confirm how the raw response identifies reposts.
- Build an honest-post candidate set without reposts.
- Compare the repost-filtered count and rate with the naive result.
Inspect the repost signal
Each returned post can include an is_retweet field. A value of true identifies a repost using the API response itself.
- Open desearch_posts.json from the folder that contains your audit script.
- Use your editor's in-file search to find "is_retweet": true.
- Read the search counter to determine how many returned rows are marked as reposts.
Every match is a row that should leave the honest-post candidate set. The search confirms that your filter has a machine-readable signal to follow.
Build the repost-free candidate set
A fair comparison changes only the post population. The engagement fields and rate formula stay exactly as they were for the naive calculation.
Why reposts change the rate
A repost row can carry a view count from that row. Its engagement numbers can describe the original post.
The filtered calculation follows this contract:
- The naive all-post count and rate remain unchanged.
- A post enters the honest-post candidate set when its is_retweet value is not true.
- The candidate set uses the existing engagement and view aggregation logic.
- The output includes the candidate post count and its repost-filtered rate.
- Open engagement_audit.py in your editor.
- Find the existing section that calculates and prints the naive post count and rate.
- Add the repost-filtering block immediately after that section by following the filtering contract above.
- Save engagement_audit.py.
Verify the filtered rate
Before you run the audit, do you expect the filtered post count to be higher or lower than the naive count?
- Run engagement_audit.py again with the same Python command you used in the previous step.
You should see the naive count and rate followed by the repost-filtered count and rate. The filtered count should drop by the number of reposts found in the raw response.
That is a meaningful correction. Your second rate now describes posts that can count as the selected account's own content.
Filtered count did not drop?
Return to desearch_posts.json and confirm that at least one post has is_retweet set to true.
If matches exist, confirm that the filtered calculation receives the candidate set. The naive calculation should continue to receive every returned post.
Ask for help with the repost filter.
✔️ Awesome, I've got everything!
Your script preserves the naive result. It now prints a second result calculated from posts that are not marked as reposts.
ⓧ I'd like to double check the full code
Compare the complete engagement_audit.py file with this cumulative behavior:
- The script loads desearch_posts.json and reads the returned posts.
- The script calculates the naive post count and aggregate engagement rate from every returned post.
- The script retains the naive count and rate after applying the new filter.
- The script identifies reposts from the is_retweet metadata field.
- The script excludes identified reposts from the honest-post candidate set.
- The script calculates the candidate rate with the same engagement and view logic used by the naive calculation.
- The script prints the repost-filtered post count and rate after the naive result.
Your audit now separates reposted content from the account's own posts. Next, you will remove repeated posts so pagination overlap cannot inflate the count.
Dedupe the Post List
Your audit now excludes reposts from the honest-post candidate set. Next, you will stop repeated post records from inflating that set.
A Desearch response can contain the same post across pagination boundaries. A stable post identifier lets your script retain one copy of each eligible post.
In this step, get ready to:
- Capture the repost-filtered candidate count as a baseline.
- Deduplicate eligible posts with their stable identifiers.
- Rerun the audit to confirm the candidate count drops.
Capture the current candidate count
The current script counts every non-repost record returned by Desearch. Running it now gives you the baseline that the deduplication stage must improve.
Before you run the audit, do you think its current candidate count can include the same post more than once?
- Run the current audit by entering this command:
python3 engagement_audit.py
What does this check prove?
Python executes engagement_audit.py in its current state. The printed repost-filtered count becomes your baseline before deduplication.
- Find the repost-filtered candidate count in the terminal output.
- Record the baseline here: your repost-filtered candidate count.
This baseline includes every eligible record returned by the API. Pagination overlap can make repeated identifiers contribute more than once.
Did the audit stop before printing a count?
Confirm that desearch_posts.json remains beside engagement_audit.py. The script needs that saved response to rebuild the candidate set.
Check the terminal output for the line number that caused the failure. Compare that line with the working script from the previous step.
Help me diagnose why my engagement audit no longer prints the repost-filtered candidate count.
Deduplicate posts by stable identifier
Deduplication belongs after the existing repost filter. This placement preserves the naive all-post result while cleaning only the honest-post candidate set.
How does identifier-based deduplication work?
- The stable post identifier becomes the key in a Python dictionary.
- The first eligible record claims that key.
- Any later record with the same key cannot create another candidate.
- The dictionary values become the deduplicated candidate list.
- Open engagement_audit.py in your code editor.
- Find the section that finishes building the eligible non-repost candidate set.
- Add the identifier-based deduplication stage described above immediately below that section.
- Save engagement_audit.py.
Before you rerun the audit, do you expect the candidate count to stay level or drop?
- Test the deduplicated candidate set by running:
python3 engagement_audit.py
What does this run test?
The script rebuilds the same repost-filtered candidate set. It then keeps one record for each stable post identifier before calculating the downstream count.
- Compare the new candidate count with your repost-filtered candidate count.
You should see a lower candidate count when the returned pages contain repeated posts. That drop is the evidence that duplicate identifiers no longer inflate the audit.
Did the candidate count stay the same?
Confirm that every eligible post contributes its stable identifier as the dictionary key. Using the complete post record as the key does not collapse repeated copies.
Confirm that the downstream candidate list comes from the dictionary values. Counting the original repost-filtered list preserves the duplicates.
Help me find why repeated stable post identifiers are still being counted more than once.
Print the before and after counts
A lower count proves that deduplication changed the dataset. Printing both counts makes that evidence visible every time you run the audit.
- Add a before-and-after candidate count comparison to engagement_audit.py.
- Save engagement_audit.py.
Before the final run, how many repeated records do you expect the comparison to remove?
- Verify the completed deduplication flow by running:
python3 engagement_audit.py
What does the final comparison show?
The first count measures eligible records after repost exclusion. The second count measures unique eligible posts after identifier-based deduplication.
The naive all-post calculation remains available for the final comparison later in the project. This step changes only the candidate list used by the honest calculation.
You will see the candidate count before deduplication beside the candidate count after deduplication. The after count should be lower because repeated identifiers now contribute once.
Missing one of the two counts?
Check that the before count reads the repost-filtered candidate set before the dictionary is applied. Check that the after count reads the deduplicated dictionary values.
Keep both output statements after their corresponding counts have been calculated. Moving an output above its calculation can stop the script.
Help me fix the missing before or after count in my deduplication comparison.
Good work. Your audit now counts each eligible post once even when returned pages overlap.
✔️ Awesome, I've got everything!
Your saved engagement_audit.py now preserves the naive calculation. It also removes reposts before deduplicating eligible posts.
ⓧ I'd like to double check the full code
Compare your complete script against these cumulative logic checkpoints.
- Confirm that the script loads the saved response from desearch_posts.json.
- Confirm that the naive aggregate rate still uses all returned posts.
- Confirm that repost exclusion runs before deduplication.
- Confirm that eligible posts are deduplicated by their stable post identifiers.
- Confirm that the script prints the candidate count before deduplication.
- Confirm that the script prints the candidate count after deduplication.
Your candidate list now represents unique original posts. Next, you will keep posts with missing view counts out of the per-view maths.
Print the Honest Engagement Rate
A per-view engagement rate depends on a usable view count. Posts with missing or unusable views cannot support that division.
Your Python audit already excludes reposts. It also removes duplicate posts across returned pages.
This final step turns those filtered posts into an audit table. You will then place the honest rate beside the naive rate to reveal the overstatement.
In this step, get ready to:
- Classify each deduplicated post by whether its view count supports per-view maths.
- Print the evidence behind each inclusion decision in a per-post table.
- Compare the honest engagement rate with the preserved naive rate.
Classify posts by view count
A valid denominator must be a positive number. Your script therefore needs a clear inclusion status for every deduplicated post.
Which view counts qualify?
- A positive numeric view count qualifies for per-view maths.
- A missing view count does not qualify for per-view maths.
- A nonnumeric or nonpositive view count does not qualify for per-view maths.
- Every deduplicated post still appears in the audit output so each decision stays visible.
- Switch back to engagement_audit.py from the previous step.
- Implement a view-eligibility audit inside the existing loop with included plus excluded counts below the deduplication counts.
- Save engagement_audit.py.
Before you run the script, do you expect every deduplicated post to qualify for per-view maths?
- Return to the terminal session from the previous step.
- Run the script again with the same command you used in the previous step.
You will see a new included total. You will also see an excluded total.
Good progress. Your audit can now defend which posts enter the denominator.
Counts missing?
Make sure the view classification runs after the existing deduplication logic.
Check that the new totals print after every deduplicated eligible post has been classified.
Help me debug the missing view-eligibility counts in my engagement audit.
Print the per-post audit table
A final rate is trustworthy when someone can inspect the evidence behind it. The per-post table makes every inclusion decision traceable.
What belongs in each row?
- The post identifier connects the row to one stable post.
- The engagement columns show the same metrics used by the existing naive calculation.
- The view column shows the value found in the saved response.
- The inclusion-status column explains whether that row contributes to the honest rate.
- Return to engagement_audit.py from the terminal.
- Add the per-post table output below the existing candidate counts using the audit rows you just classified.
- Save engagement_audit.py.
Before you run the script, which posts do you expect the table to mark as excluded?
- Return to the current terminal session.
- Run the script again with the same command from the previous step.
You will see one row for every deduplicated eligible post. Each row identifies the post.
The remaining columns show its engagement evidence plus its view value. The final column explains whether the post enters the per-view maths.
That is the audit trail in place. Every exclusion now has visible evidence.
Table rows misaligned?
Confirm the table-printing loop uses the audit-row collection created by the view classification.
Check that every row prints the same number of fields as the table header.
Help me fix the per-post table in my engagement audit.
Compare honest and naive rates
The honest account rate uses engagement from included rows divided by views from those same rows. The preserved naive rate still shows what the unfiltered calculation claimed.
How does the final comparison work?
- The honest rate uses only original deduplicated posts with usable view counts.
- The naive rate keeps the original all-post calculation unchanged.
- The overstatement subtracts the honest rate from the naive rate.
- Reporting that difference in percentage points makes the distortion easy to read.
- Return to engagement_audit.py from the terminal.
- Implement the final account summary below the per-post table using the comparison rules above.
- Save engagement_audit.py.
Before you run the completed audit, do you expect the naive rate to be higher than the honest rate?
- Return to the current terminal session.
- Run the completed script with the same command from the previous step.
You will see the per-post table above the account summary. The summary places the naive rate beside the honest rate.
A final line reports how much the naive calculation overstated engagement. This is the visible proof that the filtering rules changed the result.
You made the calculation explain itself. Your script now shows the final rate together with the evidence behind it.
Final rate looks wrong?
Check that the honest totals consume only rows marked for inclusion.
Inspect the excluded table rows if the honest denominator does not match the usable view counts.
Help me verify the final honest engagement-rate calculation.
✔️ Awesome, I've got everything!
Your completed audit exposes every inclusion decision before presenting the honest account-level result.
ⓧ I'd like to double check the full code
The completed engagement_audit.py file should satisfy every checkpoint below.
- Confirm the script loads desearch_posts.json.
- Confirm the script preserves the naive aggregate calculation across all returned posts.
- Confirm the script excludes reposts before building the honest candidate set.
- Confirm the script deduplicates eligible posts by their stable post identifier.
- Confirm the script excludes missing or unusable view counts from every per-view calculation.
- Confirm the script prints a per-post table with identifiers plus engagement evidence plus views plus inclusion status.
- Confirm the script prints the honest rate beside the naive rate.
- Confirm the script reports the naive overstatement.
Secret mission
Build a Defensible Engagement Brief
A credible metric needs a visible trail from raw posts to the reported result. Turn your audit table into a concise brief that another analyst can review.
Clean Up Your Resources
Clean Up Your Resources
Your audit files sit locally without running a background process, so keeping them creates no ongoing local compute cost. Choose a cleanup option for your files plus your Desearch access.
Resources you used:
- Local audit files: engagement_audit.py plus desearch_posts.json.
- Terminal credential: DESEARCH_API_KEY in the current terminal session.
- Desearch API key: the key created in the dashboard for authenticated requests.
Keep everything running
No action is needed. Choose this if you plan to fetch fresh posts or rerun the audit.
- Keep engagement_audit.py beside desearch_posts.json.
- Leave the Desearch API key active only while you plan to make more authenticated requests.
- Set DESEARCH_API_KEY again in each new terminal session before making another authenticated request.
Pause - I'll come back to this later
Closing the current terminal clears the session-only credential. Your two audit files remain available for your return.
- Close the current terminal window to clear DESEARCH_API_KEY from that session.
- Keep engagement_audit.py beside desearch_posts.json.
- Leave the Desearch API key available in the dashboard if you plan to fetch fresh posts later.
- Set DESEARCH_API_KEY again using the secure method from earlier when you return.
Delete - I don't want to use this again
Deleting the local audit files is permanent. Revoking the API key stops that credential from authorizing future requests.
- Return to the signed-in Desearch dashboard from earlier.
- Navigate to the area that lists your API keys.
- Locate the key you created for this project.
- Use the key's revoke control to deactivate it.
That key can no longer authorize authenticated requests after revocation.
macOS
- Press Cmd+Space to open system search.
- Search for engagement_audit.py.
- Open the file's containing folder from the search result.
- Select engagement_audit.py.
- Hold Cmd while selecting desearch_posts.json.
- Move both selected files to the Trash.
- Empty the Trash to remove both files permanently.
System search should no longer find either audit filename.
Windows
- Press the Windows key to open system search.
- Search for engagement_audit.py.
- Open the file's containing folder from the search result.
- Select engagement_audit.py.
- Hold Ctrl while selecting desearch_posts.json.
- Move both selected files to the Recycle Bin.
- Empty the Recycle Bin to remove both files permanently.
System search should no longer find either audit filename.
Linux
- Open the search feature in your file manager.
- Search for engagement_audit.py.
- Open the file's containing folder from the search result.
- Select engagement_audit.py.
- Hold Ctrl while selecting desearch_posts.json.
- Move both selected files to your system trash.
- Clear the trash to remove both files permanently.
Your file manager should no longer find either audit filename.
- Close the current terminal window to clear DESEARCH_API_KEY from the session.
Nice Work!
Nice Work!
You made it! You built a Python audit that turns raw X account post data into an honest engagement-rate comparison.
You've learned how to:
- Fetch recent account posts through the Desearch API. Preserve the response as raw JSON for a repeatable audit.
- Calculate a naive engagement rate to reveal the misleading baseline. Exclude reposts from the honest calculation. Deduplicate repeated posts by stable identifier.
- Keep missing or unusable view counts out of per-view maths. Print a per-post audit table. Compare the honest rate with the naive rate. Report how much the naive result overstated engagement.
- Secret Mission: Complete an optional challenge to push your engagement-audit skills further.
Ready to quiz yourself?