If you manage more than a handful of social media or ad accounts — for clients, for several brands, or for testing campaigns — you've probably hit the moment where "I'll remember it" stops working. Which account has 2FA on an authenticator app vs. SMS? When was the password last rotated? Which recovery email belongs to which profile? Which account hasn't been logged into for six weeks?
You don't need a SaaS dashboard for this. A well-structured spreadsheet plus a 60-line script gets you most of the way. This post walks through the schema I'd recommend, a few rules that keep it safe, and a small Python script that turns the sheet into a daily "what needs attention" report.
1. Rule zero: the sheet is an inventory, not a vault
Before anything else: do not put passwords, 2FA seeds, or backup codes in the spreadsheet. Those belong in a password manager (Bitwarden, 1Password, KeePassXC, etc.). The sheet stores metadata — what exists, where its secrets live, and when things were last checked.
That separation matters because spreadsheets get shared, exported, emailed and synced to places you forget about. A leaked inventory is annoying; a leaked inventory with credentials is a disaster.
2. The schema
One row per account. Here are the columns I've found useful:
| Column | Example | Why |
|---|---|---|
id |
fb-003 |
Stable short ID you can reference anywhere |
platform |
facebook |
Lowercase, consistent values for filtering |
handle |
@brand_eu |
Public name/handle |
purpose |
ads-eu |
What the account is for |
owner |
anna |
Who is responsible |
vault_ref |
Bitwarden/Clients/fb-003 |
Where the secrets live |
twofa_method |
totp / sms / none
|
Spot weak setups fast |
recovery_email |
ops+fb003@… |
Must be an inbox you control |
browser_profile |
profile-07 |
Which isolated profile logs into it |
last_login |
2026-10-28 |
Detect dormant accounts |
last_pw_rotation |
2026-08-01 |
Rotation reminders |
status |
active / review / restricted
|
Current health |
notes |
free text | Context for teammates |
A few tips:
-
Use data validation (dropdowns) for
platform,twofa_methodandstatus. Free-text values like "FB", "facebook", "Facebook " will quietly break your filters. -
Use ISO dates (
YYYY-MM-DD). They sort correctly and parse without surprises. -
Plus-addressing (
ops+fb003@yourdomain) gives every account a distinct recovery address while keeping one inbox.
3. Health checks worth automating
Once the data is structured, "health" becomes a set of simple rules:
- No 2FA, or SMS-only 2FA → flag. Authenticator apps or hardware keys are more resistant to SIM-swap issues.
-
No
vault_ref→ flag. If nobody knows where the credentials are, the account is effectively unowned. -
Dormant:
last_loginolder than N days (I use 30) → flag. Some platforms treat long-idle accounts with more suspicion, and you also lose track of whether they still work. -
Rotation due:
last_pw_rotationolder than 90 days → reminder. -
Status not
active→ always surface it. -
Duplicate
browser_profileacross accounts of the same platform → flag, if your policy is one profile per account.
None of this is clever. The value is that it runs every day without anyone remembering to do it.
4. The script
Export the sheet as CSV (or pull it via the Google Sheets API later — start simple). Then:
#!/usr/bin/env python3
"""Daily health report for a social/ad account inventory CSV."""
import csv
import sys
from collections import defaultdict
from datetime import date, datetime
DORMANT_DAYS = 30
ROTATION_DAYS = 90
today = date.today()
def days_since(value):
if not value:
return None
return (today - datetime.strptime(value.strip(), "%Y-%m-%d").date()).days
def check(row):
issues = []
if row["twofa_method"] in ("", "none"):
issues.append("no 2FA")
elif row["twofa_method"] == "sms":
issues.append("SMS-only 2FA")
if not row["vault_ref"].strip():
issues.append("no vault reference")
d = days_since(row["last_login"])
if d is None or d > DORMANT_DAYS:
issues.append(f"dormant ({d if d is not None else '?'} days)")
r = days_since(row["last_pw_rotation"])
if r is None or r > ROTATION_DAYS:
issues.append("password rotation due")
if row["status"] != "active":
issues.append(f"status={row['status']}")
return issues
def main(path):
with open(path, newline="", encoding="utf-8") as f:
rows = list(csv.DictReader(f))
profiles = defaultdict(list)
for row in rows:
if row["browser_profile"]:
profiles[(row["platform"], row["browser_profile"])].append(row["id"])
flagged = 0
for row in rows:
issues = check(row)
key = (row["platform"], row["browser_profile"])
if len(profiles.get(key, [])) > 1:
issues.append(f"shares profile with {', '.join(i for i in profiles[key] if i != row['id'])}")
if issues:
flagged += 1
print(f"[{row['id']}] {row['platform']} {row['handle']} ({row['owner']}): " + "; ".join(issues))
print(f"\n{flagged}/{len(rows)} accounts need attention on {today}.")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else "accounts.csv")
Sample output:
[fb-003] facebook @brand_eu (anna): SMS-only 2FA; password rotation due
[tt-011] tiktok @brand_test (marc): dormant (41 days)
[x-002] x @brand_support (anna): shares profile with x-005
3/24 accounts need attention on 2026-11-02.
5. Running it on a schedule
-
cron on any always-on machine:
0 9 * * 1-5 python3 report.py accounts.csv | mail -s "Account health" ops@yourdomain - GitHub Actions with a scheduled workflow, if the CSV lives in a private repo (keep it private — it's still sensitive metadata).
- Google Apps Script, if you'd rather stay inside Sheets. A trimmed-down equivalent:
function dailyHealth() {
const rows = SpreadsheetApp.getActive().getSheetByName('accounts')
.getDataRange().getValues();
const head = rows.shift();
const col = n => head.indexOf(n);
const now = new Date();
const out = rows.filter(r => {
const idle = (now - new Date(r[col('last_login')])) / 864e5;
return r[col('twofa_method')] !== 'totp' || idle > 30 || r[col('status')] !== 'active';
}).map(r => `${r[col('id')]} ${r[col('handle')]}`);
if (out.length) MailApp.sendEmail(Session.getActiveUser().getEmail(),
'Accounts needing attention', out.join('\n'));
}
Add a time-driven trigger and you get a morning email.
6. Onboarding new accounts
The workflow is most useful at the moment an account enters your hands — whether you created it, inherited it from a client, or purchased it. A short checklist, mirrored as columns in the sheet:
- Log in from the browser profile you'll use going forward and record it.
- Change the password and store it in the vault; fill
vault_refandlast_pw_rotation. - Replace the recovery email/phone with ones you control.
- Enable authenticator-app 2FA and save backup codes in the vault.
- Review active sessions and connected apps; revoke anything unknown.
- Set
status=activeonly once all of the above are done.
If you source ready-made accounts for ads or testing — from a provider such as Huoxing Shop or anywhere else — running every new account through this same checklist on day one keeps your inventory consistent no matter where an account came from.
Wrapping up
The whole system is: one sheet with strict columns, secrets kept elsewhere, and a tiny script that turns rules into a daily list. It scales comfortably to a few hundred accounts, and when it stops scaling you'll already have clean data to move into something bigger.
Author note: I write about practical account operations for Huoxing Shop (火星小铺), a store for social media and ad accounts across Facebook, Google, TikTok, X, Instagram and Telegram — see hxpuzi.com. Questions: support@hxpuzi.com.
Top comments (0)