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 -1If 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"}
EOFA 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 1Check .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.