Chapter 34. Spend Discipline for a One-Person Estate

Monthly bills grow through accurate invoices for work nobody is doing. On 2026-07-16 I ran a billing audit across the estate and found two charges I could not defend: $20 a month for a single 188-byte file, and a Render service that had been billing since its 2026-07-14 deploy without finishing one job. Neither appeared as a failure on any dashboard. Both invoices were accurate.

That is how money leaves a one-person estate. Not through a bad decision you remember making, but through a decision that was correct on the day you made it and kept billing after the reason for it went away. Nobody sends you a notice. The provider is happy, the service is green, and the charge renews.

The free-tier threshold trap: the price step ignores size

Pinata’s Free tier stops at 500 pinned files. I was on the Picnic plan at $20/mo. When I opened the account to see what the money was buying, the pinned-file count read 501.

The 501st was a leftover test metadata record. 188 bytes. It had been written during a registry upload test, served its purpose in that session, and then sat there paying rent. Unpinning it dropped the count under the cap, and the downgrade to Free is scheduled to take effect 2026-07-24.

The arithmetic here deserves attention. The account was not on a paid tier because of usage. It was on a paid tier because of a threshold, and thresholds do not care which artifact crossed them. The 188-byte file cost exactly the same as the 500 records that preceded it, because the price step is attached to the count and not to the bytes. Every provider with a free tier has a shape like this somewhere: a file count, a row count, a seat, a project, a domain. Find the ceiling before you find the invoice.

The counter is also easy to trip a second time. The next registry or IP upload batch can push me back over 500 without anyone noticing, so the cap goes on the list of things I check before a batch, not after a bill.

Test artifacts are the recurring culprit because they are indistinguishable from production records by design. That is what makes them useful during a test and expensive afterward. If a test writes to a metered store, the test owns the cleanup.

A paid service with zero completed jobs

Render was projecting about $16.83 for July across two Starter services. Modest, which is part of why my eye slid past it on previous passes over the bill.

producer-suedelabs-worker had zero successful job logs. What it had instead were database connection failures, running back to its 2026-07-14 deploy. It had never completed a single unit of work in its entire billed life. It booted, failed to reach the database, retried, and stayed up.

Nothing caught this because nothing was asking the right question. The monitoring I had confirmed the process was running. The process was running. It was running the way a car with no wheels runs: engine on, going nowhere, burning fuel. Render’s dashboard showed a live service because a live service is what it was.

Check output, not liveness, on a paid background service

A health check that pings a process proves that a process answers pings. For a worker, that is close to meaningless. The thing you want to know is whether it has produced output.

The check I run now on any paid background service is a log query, not a ping:

# for each paid service: when did it last finish a job?
render logs --service <service> --limit 500 | grep -iE 'job (complete|succeeded|finished)' | tail -1

If that returns nothing over the window you care about, you own a subscription with a heartbeat. Three columns go in the audit note for every paid service: the date of its last successful job, what depends on its output, and what breaks if I suspend it today. The third column is the one I have skipped most often, and skipping it is what turns a cost fix into an incident.

The generalization runs past workers. A cron that fires and exits 0 without doing work is green. A queue consumer holding an idle connection is green. A serverless function that falls back to a non-durable store and returns 200 is green, which is a failure mode I have hit for real in Agent Studio and which cost me a production outage rather than money. Green is a claim about the process. Output is a claim about the work.

A tier downgrade is a change to production

I suspended producer-suedelabs-worker, and it stopped billing. Then I downgraded Suede-AI-App, the live x402 API service, to Render’s Free tier.

The second move is the one worth sitting with. Suede-AI-App is not a side project or a staging box. It handles live payment calls. Render’s Free tier can cold-spin-down after 15 minutes idle, with less CPU and traffic headroom than Starter. I took a service that settles money and put it somewhere it can be asleep when the first request of the hour arrives.

That risk is open. It has not been mitigated, tested under load, or accepted after measurement. It is written down as a watch item: watch for latency or reliability regressions on live x402 payment calls. Writing it down is the entire discipline. The alternative is a note that reads “cut Render spend, done,” which describes a clean win that did not happen.

The suspension carries its own tail. Any queued or scheduled jobs that worker owned will not run again until someone resumes it. I have not confirmed what, if anything, depended on it, so the second open item reads: check whether anything depended on that worker before assuming background jobs are still happening. A worker with zero successful jobs since deploy was not doing that work either. The difference is that now the failure is deliberate, and deliberate failures should be listed as such.

Every cost cut is a change to production topology. Tier downgrades, suspensions, and plan changes deserve the same treatment as a code deploy: a record of what changed, what it saves, and what it might break. Put the saving and the risk in the same paragraph, because separating them is how the risk gets lost.

Preview builds nobody opens

Compute you never look at is the largest recurring leak I have found, and on Vercel it arrives as preview builds. I do not review preview URLs. Every branch push producing a full preview build was pure cost with no reader.

I enforce the kill at the project level, in commandForIgnoringBuildStep, not only in vercel.json, because project-level covers the zero-config gap where a repo has no vercel.json at all. Confirmed 2026-06-08. Swept all 33 projects under team team_i3zcwXswG8Kcy4hJaBIKuAt6 on 2026-06-13: 6 already off, 27 newly set, 0 failed. Measured on 2026-07-09, the 30-day window showed 1,331 preview builds canceled against 15 built.

That sweep also taught me something about bulk config edits. It wrote one universal command to every project without reading what was already there, which flattened the path-scoped ignore commands on six monorepo sub-projects: ip to landing-site/, suede-ai-app to frontend/, suede-home to suede-home/, suede-launch to launch/, plus launch and frontend. Those commands were a superset of what I was installing, since they killed previews and also skipped production builds when the project’s own subdirectory had not changed. All six were restored. A bulk sweep has to diff and merge.

The monorepo case is where the real money sits. The nft project carried only the plain preview-kill, so it kept building on every production push to the monorepo whether or not nft/ had changed: 189 wasted production builds in 30 days. The fix landed in both the project setting and nft/solana-nft-creator/vercel.json as commit 9765f1e7, because vercel.json overrides the dashboard and a dashboard-only fix would have done nothing.

Preview-kill controls what CI does and has no opinion about what an agent does by hand. suede-promo recorded 287 CLI production deploys in 30 days, 123 of them on 2026-06-25 alone, from agent loops calling vercel --prod once per iteration instead of iterating locally. The countermeasure is written into the repo the agent reads: one vercel --prod per session, at the end, iterate locally with python3 -m http.server 8717 or vercel dev. That policy went into JasonColapietro/suede-promo’s CLAUDE.md as commit 1fbc4cd, and the HANDOFF.md deploy lines point at it. Worth noting for that repo: its Vercel project is not git-linked, so pushing does not deploy and deploying does not push, and an agent has to do both, once.

One more trap from that audit. I patched the stored ignore command through an unquoted bash heredoc, which let my local shell expand $VERCEL_ENV to an empty string before the JSON went out. The stored command then skipped production builds along with previews. Caught and re-patched in the same session with a quoted heredoc:

curl ... -d @- <<'EOF'
{"commandForIgnoringBuildStep":"[ \"$VERCEL_ENV\" != \"production\" ] && exit 0 || exit 1"}
EOF

A cost control that disables your production deploys is worse than the cost.

While I am naming levers: the bill is builds and compute. Vercel’s PR and commit comments are free, so turning them off saves nothing and costs me review signal.

A per-tenant free allowance with unbounded tenants

Spend discipline runs in both directions. The Suede Agent Studio LLM gateway grants FREE_MONTHLY_GATEWAY_TOKENS=100k per workspace, and workspaces can be self-minted without limit. The allowance is per tenant, tenant creation is unbounded, and the tokens burn against a real Anthropic key. In aggregate the free tier has no ceiling.

This is not fixed. Per-IP caps or workspace gating are needed before I promote the gateway, and until that lands the gateway stays unpromoted. A per-tenant free tier is bounded only if tenant creation is bounded.

The same reasoning produced the rule for metered external-tool nodes in Agent Studio: every execution bills the workspace, including a builder’s own draft or test run in the canvas. No free tier, no subsidized testing, no free monthly allowance, no distinction between a published run and a test run. One universal billing rule removes the edge case rather than defending it.

When to suspend or kill a service

Suspending producer-suedelabs-worker was cheap, immediate, and reversible, and it was the correct call for a service that had never produced output. That is the easy version. The harder version is a project that works, costs money, and is not going anywhere. I killed the Virtuals ACP agent outright on 2026-06-12 rather than keep maintaining a bet I had stopped believing in. The estate kept the Render backend it depended on, because the live x402 endpoints proxy through it. I have no clean monthly figure to attach to that decision, which is its own small indictment of how I was tracking spend at the time.

Not promoting the gateway is the same instinct applied earlier in the lifecycle. A surface I do not push traffic to does not need per-IP caps this week.

An audit that returns nothing is not a clean estate

The last leak is the audit itself. When I swept repos for the ignore command on 2026-06-08, three methods lied to me. GitHub code search lags and skips repos. Un-throttled recursive tree calls hit secondary rate limits and return empty results rather than an error. Root-only contents checks miss nested configs in web/ or frontend/.

Each failure mode returns an empty list, and an empty list reads as a clean estate. The replacement is a throttled per-repo sweep against the authoritative endpoint:

gh api "repos/$O/$R/git/trees/$BRANCH?recursive=1" --jq '.truncated, (.tree[].path)'
sleep 1

Check .truncated on every response. A truncated tree is a partial answer wearing the costume of a complete one, which is the same disease as a live service with no completed jobs and a $20 charge for 188 bytes. In each case the system reported its own state and the report was true and useless. What I audit now is output: jobs finished, files counted, builds run, tokens spent against a key I own.