
Never let a robot spend your money while you sleep
What you will learn
- Why the dangerous bill is the small quiet one, not the dramatic one
- Four fixes in a deliberate order, each useless without the one before it
- The check I skipped with total confidence, which cost me real money
The difference between a hobby and a bill is whether anything you built can spend money while you are asleep. Once an automatic job can reach a paid service, you have handed a credit card to something that does not get tired, does not get bored, and will happily keep trying all night.
This is not a warning about enormous disasters. Those get noticed. The dangerous version is quiet and small: a scheduled job that costs a little every night and never tells anybody.
You do not notice a slow leak until it has been running long enough to matter, because nothing about it ever looked wrong.
There are four moves that fix this, and the order is deliberate, because each one is worthless without the one before it.
One: see it
You cannot manage what you cannot look at. Before any cleverness, record what every paid request cost and put the total somewhere you will actually see. Not a file you would have to remember to open. A number on a page you already look at.
Almost everybody who gets surprised by a bill was flying blind, not being reckless.
Two: predict it, then pause
Before a big job runs, estimate what it will cost, and set a number that stops it.
Your estimate will be wrong, and it will be wrong in the expensive direction.
I have watched a prediction come in many times under the real cost, more than once, and each time the reasoning had looked perfectly sound. Treat every estimate as a floor, not a middle. If the real number races past it, stop the job and think again rather than letting it run.
And measure with the tool's own counter rather than your mental picture. My worst predictions all came from estimating the work myself instead of asking the software what it was actually about to process.
Three: check the environment, not just the settings file
This one cost me real money for an embarrassing reason.
I confirmed the settings file held no credentials and concluded the job could not possibly spend anything. It spent anyway, because the key it found was sitting in the surroundings the program ran in, set somewhere else entirely and inherited without anyone noticing.
The settings file was genuinely clean. I had checked the wrong thing with complete confidence.
When you want to know what a program will do, inspect the conditions it will actually run in, not the file you happen to associate with those conditions.
Four: fail cheap, cap, and cage
Set every automatic job to use the free or local option by default, and make the paid one an explicit choice rather than a fallback. A job that quietly upgrades to the expensive path when the cheap one is unavailable is the exact shape of a surprise bill.
And if a job must never spend, do not rely on it choosing well. Remove the keys from its surroundings entirely, so the expensive path is unreachable rather than merely discouraged.
That last distinction is the whole discipline. Discouraged means it will happen eventually. Unreachable means it cannot. Anywhere you can turn the first into the second, do it, and then you get to stop worrying about that one forever.
An unattended job with no ceiling is a bill that has not arrived yet.
Cost visibility, a gate that pauses, the environment check, and how to make the expensive path structurally impossible. None of this needs a particular provider.
1. Record every paid request
What this does: writes one line every time something costs money: what it was for, how much work it did, and what it cost.
Why start here: it is the foundation for everything else and it takes ten minutes.
def log_cost(source, units, dollars):
with open("spend.log", "a") as f:
f.write(f"{time.time()}\t{source}\t{units}\t{dollars:.4f}\n")
2. Total it somewhere you will see it
What this does: adds up today's spending by source and lists the biggest first.
The important half: put that number on a page you already open. A number you have to go looking for is a number you will not look at.
awk -v d="$(date +%s -d today)" '$1>d {s[$2]+=$4} END \
{for (k in s) printf "%-18s %.2f\n", k, s[k]}' spend.log | sort -k2 -rn
3. Ask the tool what it is about to process
What this does: gets a real count from the software instead of you guessing.
Why it matters: every bad prediction I made came from modelling the work in my head rather than asking.
scope = tool.scan(path, exclude=["vendor/", "node_modules/"])
print(scope.file_count, scope.total_words)
4. Set a ceiling and stop at it
What this does: turns that count into an estimate, compares it against a limit you chose in advance, and refuses to start if it is over.
Why it prints either way: so the estimate itself gets checked against reality afterwards.
CEILING = 200_000 # what you are willing to spend unattended
est = estimate(scope)
print(f"projected: {est} (floor, expect higher)")
if est > CEILING:
sys.exit(f"STOP: projection {est} over ceiling {CEILING}")
5. Check the environment, not the settings file
What this does: asks the program what credentials it can actually see right now.
Note the ending: it prints names only, never the values. Never print a secret, not even to check it.
This is the check I skipped, and it is one line.
env | grep -iE '_(API_)?KEY|_TOKEN|_SECRET' | cut -d= -f1
6. Make the expensive path unreachable
What these do: strip the credentials out of the job's surroundings, so a paid provider cannot be chosen at all.
Why both lines: one clears them in the job's definition, the other clears them again at the moment it runs, so it holds however the job gets started.
# in the job definition
UnsetEnvironment=VENDOR_A_KEY VENDOR_B_KEY VENDOR_C_KEY
# and again where it is actually run
env -u VENDOR_A_KEY -u VENDOR_B_KEY -u VENDOR_C_KEY ./nightly-job.sh
7. Verify the cage rather than trusting it
What this does: before the job starts, counts how many of those keys are visible and refuses to continue unless the answer is zero.
Why bother: absent by luck is not the same as absent by design, and this is the difference between the two.
ExecStartPre=/bin/sh -c 'env | grep -ciE "VENDOR_._KEY" | grep -qx 0'
8. Default to free, cap each source
What this does: once a source passes its daily limit, it falls back to the local free option instead of continuing to spend.
The direction matters: degrade, never escalate. Anything that runs unattended should also be read-only where possible, so the worst case is wasted time rather than money or damage.
if spend_today(source) > CAP[source]:
return use_local() # degrade, never escalate
9. Watch the first unattended run
The first scheduled run after any change is the one that surprises you. Check the spending record the next morning and compare it against your prediction.
That comparison is how your estimates stop being wrong.
Related: give every job its own key, so a limit and a leak both stay contained.
This page in the original Kyber Cypher voice: Never let an AI job spend money unattended