My homepage shows my Claude bill. June was $13,370.
A six-month spend sparkline, refreshed hourly from my own FinOps API, sits in my homepage's proof strip. The whole integration is 198 lines, 39 lines of Terraform, and one scoped token. Here is why I published the bill, and how it never breaks.

$13,370. That was June. Metered Claude spend, one vendor, one email: mine.
It renders on my homepage. A six-month stacked sparkline, refreshed hourly, fed by a live GraphQL query. Feb $423. Mar $849. Apr $1,752. May $11,725. Jun $13,370. July so far: $54 in tokens, plus $360 flat for three Claude Team seats. Trailing six months: $28,173 in metered tokens.
Not a screenshot, a live query against my own FinOps platform, on a personal site. The whole thing: 198 lines of TypeScript, 39 lines of Terraform, one scoped token, and a fallback value that says $26 while the real number was five hundred times bigger.
Claims are cheap
My hero copy makes a hard claim: "15,000+ GitHub contributions since March, all AI-written, none typed by hand." That line is either the most interesting thing on the page or the least believable. In 2026 everyone says AI-first. Decks say it. LinkedIn says it. Vendor blogs say it in the passive voice. A claim without a meter is a slogan.
So I wired the meter to the page. Two of the four stats in my proof strip are live: GitHub contributions over the last year, and the Claude bill. Not on the hero. Receipts belong next to the argument, so they sit in the first section after the fold, where the "what changed" story gets told.
The bill does two jobs at once. It proves the usage is real. And it shows what the usage costs. That second job is the one everybody skips. A $13,370 month is not a flex. It is a number that can embarrass me, and that is why it works as proof. Anyone can publish a number that flatters them. Readers smell curated.
198 lines, zero new services
The entire live-stats layer is one server-only file, src/lib/stats/live.ts, 198 lines in a ~12,650 LOC personal site. No cron. No database on the site side. No new service. A stat opts in through a source field on the authored content, Zod-validated to a two-value union: "githubContributions" | "tokenSpendMtd".
The GitHub stat is dumb on purpose. It scrapes the public contributions page and regex-parses the total out of the heading. Seven lines. No token, no API client, no GraphQL schema to chase. The tests feed it irregular whitespace, the singular "1 contribution" form, and a rate-limited error page. The first two parse. The third returns null, and null is fine.
The spend stat is one query:
// src/lib/stats/live.ts:159, the entire query
"query ($range: AiCostRange!, $email: String!) { aiCostUserSeries(range: $range, vendor: CLAUDE_AI, email: $email, granularity: DAY) { day costMicros } }"
Both fetches run in a Promise.all inside one unstable_cache wrapper: revalidate: 3600, tag live-stats, an AbortSignal.timeout(8_000) per fetch. The numbers refresh hourly at render. That is the whole runtime footprint.
The API behind that query is meridian, the FinOps platform I built for tracking AI spend across the org: roughly 300K lines of non-generated, non-test Go, 59 GraphQL schema files, 195 scope directives, 12 aiCost* query fields, an 8-vendor enum. The homepage eats from the same aiCostUserSeries field that backs the org dashboard's per-user drilldown. If the platform is good enough to bill teams with, it is good enough to put my own name on, in public.
One thing about that number's provenance, because it matters. Meridian's CLAUDE_AI vendor is per-user spend ingested from Anthropic's Enterprise Analytics: chat, Claude Code, Cowork, sliceable by product. The schema keeps it explicitly separate from the ANTHROPIC vendor, which is the raw developer-API cost report. And the same schema documents the OpenAI per-user figure as a token-share estimate. So the graph on my homepage is a bill, not a guess. Ingest is stage-and-replace: a windowed DELETE scoped to the account and day range, then insert, in one transaction. Re-running ingest cannot double-count my June.
The token is the interesting part
The site authenticates with a service token named stark-personal. Minting one is a ceremony:
# meridian/backend/internal/gql/schema/rbac.graphql:301
createServiceToken(name: String!, scopes: [ServiceTokenScopeInput!]!): MintedServiceToken!
@superAdmin @requiresHumanAttribution
The service role has no default permission grid. A token's effective permission set is its explicit grants, nothing inherited, nothing implied. This token carries one grant: platform:read. That is the only scope the query needs, so that is the only scope the token has.
The key mechanics are boring, as they should be. Format: sdc_ plus 36 random bytes base64url, 52 characters total. The middleware pre-filters on the prefix before touching the database. At rest it is a sha256 hash; the schema is blunt that the raw key appears exactly once, at mint, and is never recoverable. Rotation keeps the previous key valid until the next rotation, so a rotate never races a deploy. Env-key comparisons are constant-time.
The asymmetry is the part worth copying. A machine can use a token. Only an attributable human can create one: minting, rotating, and revoking are @superAdmin @requiresHumanAttribution. Meridian keeps a catalog of sensitive scopes (rbac, people, gdpr, connectors, and so on) where every resolver must make an explicit machine-access decision, enforced by a CI walker. platform is not in that catalog. Machine identities reading platform metrics is by design. Machine identities minting credentials is not a thing.

And the endpoint fails closed. I checked from a clean shell before writing this: an unauthenticated aiCostSummary against the public GraphQL endpoint returns UNAUTHENTICATED. Public URL, private data.
Thirty-nine lines of Terraform, one deliberate downgrade
The token lives in Secret Manager, provisioned by a 39-line Terraform file. The header comment is the design decision:
# infra/stark-personal-stats.tf
# Read-only from the app's perspective (no /admin rotation UI), so unlike the
# llm-tokens secret the runtime SA gets ONLY secretAccessor. Rotation =
# rotateServiceToken on meridian + `gcloud secrets versions add` here.
#
# Seed the first version out-of-band (values never live in Terraform/state)
The same app has another secret, the LLM API keys, where the runtime service account holds both secretAccessor and secretVersionAdder because the admin UI rotates those keys live. The stats secret gets accessor only, because nothing in the app ever writes it. Same service account, two different trust shapes, each matching what the code actually does with each secret.
The app reads the secret through the Secret Manager API at runtime instead of an env mount. So rotating the meridian token takes effect without a redeploy: rotate on meridian, gcloud secrets versions add, done.
The design spine is failure
Here is the file header's contract, verbatim: "Every failure degrades to null and the seeded static value keeps rendering, so the section can never break on an upstream hiccup."
The overlay that enforces it:
// src/lib/stats/live.ts:70, the opt-in overlay
export function applyLiveStats(stats: SiteConfig["stats"], live: LiveStats): SiteConfig["stats"] {
return stats.map((s) => {
if (s.source === "githubContributions" && live.githubContributions !== null)
return { ...s, value: live.githubContributions };
if (s.source === "tokenSpendMtd" && live.tokenSpendMtdUsd !== null)
return { ...s, value: live.tokenSpendMtdUsd };
return s;
});
}
Every layer is independently soft. Secret missing or IAM broken: null. GitHub HTTP error or regex miss: null. Meridian HTTP or GraphQL error: null. Malformed series points coerce to zero, and there is a unit test that feeds the summing code NaN. Every failed fetch emits a structured stats.live_fallback log event with the reason. One honest gap: the missing-secret path returns null before any fetch happens, so it degrades without a log line. That is on the fix list. A failed fetch pins null for one hour until the next revalidation. The month range is half-open UTC, first of the month to tomorrow, with a test for the first-of-month edge, because timezone bugs love billing boundaries.
Now the comic detail. The authored fallback for the spend stat, the value that renders if every layer fails, is $26. The real June number was $13,370. Off by five hundred x, and it does not matter at all. A fallback's job is to render, not to be right. The moment you demand an accurate fallback you have built a second live system, with worse operational properties than the first. The seed value is a shock absorber.

The chart itself is honest about what it knows. Metered tokens come from meridian. The three Claude Team seats are flat-rate, so they never appear in a metered series; the site stacks a hardcoded $360-per-month base under the token bars, labeled as such, running since July. The $360 is what the seats actually bill me each month; seat tier and tax mean you will not reproduce it from Anthropic's public price list. The sparkline is an SVG with role="img" and a full data aria-label: "Claude spend by month, tokens plus team plans: Feb $423, Mar $849, Apr $1,752, May $11,725, Jun $13,370, Jul $414". Feb through Jun in that label are tokens only; the seat base enters at July. Counters and motion gate on prefers-reduced-motion. I work at an accessibility company. The receipts widget does not get to be the inaccessible part.
What broke
The 198 lines were the fast part. Shipping them surfaced three failures, none of them AI-shaped.
CI was dead for two days and nobody noticed. The workflow YAML had unquoted colons in step names. Every run failed at startup from July 11. The changelog entry is a monument to reading your own pipeline output.
Deploy exited 0 and served old code. Cloud Run traffic was pinned to a named revision under a stale candidate tag, so new deploys took no traffic. The site looked deployed and was not. The fix landed the same day the stats shipped. Lesson burned in: exit code 0 is not "serving"; verify the traffic split, not the deploy log.
Dev mode lies. Turbopack dev cannot read Firestore here; firebase-admin's settings() double-fires under HMR and config silently falls back to defaults. The only honest verification is the production build run locally. "Works in dev" was worth nothing.
The bill on the homepage includes the tokens I burned debugging why the bill was not on the homepage. That is the texture of AI-first work in 2026: the model writes the feature in an afternoon, and the humans still own YAML quoting, traffic tags, and hot-reload ghosts.
Standing Receipts
Name the pattern so you can reuse it. A Standing Receipt is a claim wired directly to the system that proves it: a live source behind a scoped credential, an authored fallback so the page never breaks, and a number that is allowed to embarrass you.
Static proof rots. A screenshot of a dashboard was true once. A Standing Receipt re-earns the claim every hour, and its failure mode is a stale-but-plausible authored value, not a broken page. Five rules:
- Live or nothing. If the number does not refresh itself, it is decoration.
- One scoped credential, human-minted. Grant only what the query needs; mine holds a single (scope, operation) pair. Split creation from use: attribution on mint, least privilege on use. And scope per secret, not per app. Copy the accessor-only pattern for anything the app never writes.
- Fail soft, log loud. A degrade returns the authored value and logs why. Test those paths hardest: rate-limited HTML, the singular "1 contribution", NaN cost points, the first-of-month range. 171 tests pass site-wide, and the ones I trust most assert
null. - It must cut. If the number can only flatter you, it is marketing. Mine printed $11,725 in May whether I liked it or not.
- Put the receipt next to the claim. Beside the paragraph making the argument, where the skeptic is already reading.
The cost is an afternoon. 198 lines of TypeScript, 39 lines of Terraform, one minted token. The first version shipped in a single PR, +386/-82 across 14 files, on July 12; the sparkline followed a day later. If your org has any internal metrics API, you are one scoped token away from a Standing Receipt.
The contributions counter shows the pattern outrunning its own copy. The seed file says 15,806. The page served 17,562 today. GitHub said 17,567 when I checked minutes later. The receipt moves faster than I can screenshot it, which is the property you want in a receipt.
The meter is running
Twenty-five years in, I have written plenty of copy about engineering. Copy asks to be believed. This is the first page I own that does not ask. It queries.
The June bar will scroll off the chart by winter, and whatever the next months print, the page will print it: high, low, or $26 because something upstream fell over. That is the deal you sign with a Standing Receipt. You stop curating the story and let production tell it.
Wire a number to your loudest claim. Make it refresh itself. Pick one that can hurt. If the number cannot embarrass you, it cannot vouch for you either.