Last Tuesday I approved a five-step research run. It broke on step three.
The summary it produced looked fine. The silence underneath it did not.
I could not say which steps had actually run. I could not say what they cost.
So I approved an action with missing evidence. That was my call, not the model's.
Here is the question I keep asking myself now. What would have stopped me?
Not a nicer warning banner. A record, written before the spend.
This tutorial builds that record. Five stages, each with a check you can fail on purpose.
The whole thing is three small Node files and no dependencies. You can run it before lunch.
Why a ledger, and not a dashboard
A dashboard tells you what already happened. You need to know what is about to happen.
That is the decision point. Approve, refuse, or hand back — before the call, not after.
A local ledger does three jobs at once. It proves the run's spend, it carries the stop condition, and it maps recovery after a crash.
I run these calls through MonkeyCode's free model access, and the project is open source. The operator also offers a free server option.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
I treat both availability claims as operator-supplied. I do not restate any allowance figure here. Check the project page for the current numbers, then count the spend yourself.
The dashboard number is not evidence. Your record is.
Stage 1 — Write the record shape first
Design the record before you touch a model. Fields beat prompts here.
// ledger.js — append-only spend and recovery record, no dependencies
const fs = require("fs");
const PATH = process.env.LEDGER || "run.jsonl";
function read() {
if (!fs.existsSync(PATH)) return [];
return fs.readFileSync(PATH, "utf8").split("\n").filter(Boolean).map(JSON.parse);
}
function remaining() {
const rows = read();
if (!rows.length) return Number(process.env.BUDGET_START || 0);
return rows[rows.length - 1].budget_after;
}
function append(entry) {
const before = remaining();
const row = { ...entry, budget_before: before, budget_after: before - entry.tokens };
if (!Number.isFinite(row.tokens) || row.tokens < 0) throw new Error("refuse: bad token count");
if (row.budget_after < 0) throw new Error("refuse: budget would go negative");
fs.appendFileSync(PATH, JSON.stringify(row) + "\n");
return row;
}
module.exports = { read, remaining, append };
Every row needs seven things. run_id, step_id, the intent in plain words, an evidence_ref pointing at the source it read, the tokens it spent, the outcome, and whether the step is reversible.
If a field feels awkward to fill, that is the field doing the work. Awkward is honest.
Verify it with a throwaway file:
rm -f tmp.jsonl
LEDGER=tmp.jsonl BUDGET_START=10000 node -e '
const l = require("./ledger");
l.append({ run_id:"demo-01", step_id:"s1", intent:"read source",
evidence_ref:"doc-17", tokens:120, outcome:"verified", reversible:false });
console.log(l.remaining());'
# expect: 9880
If you see 9880, the arithmetic and the append are both wired. If you see NaN, your last row is missing budget_after, and you just found out before a model did.
Stage 2 — Put a gate in front of every call
The gate is the only part that can say no. Keep it dumb. Dumb gates survive refactors.
// gate.js — run before every model call
const ledger = require("./ledger");
const FLOOR = Number(process.env.FLOOR || 5000);
const step = process.argv[2];
const rows = ledger.read();
const last = rows[rows.length - 1];
if (last && last.outcome !== "verified") {
console.error(`STOP: previous step unverified — ${last.step_id} (${last.outcome})`);
process.exit(2);
}
if (ledger.remaining() < FLOOR) {
console.error(`STOP: remaining ${ledger.remaining()} below floor ${FLOOR}`);
process.exit(3);
}
console.log(`OK ${step} remaining=${ledger.remaining()}`);
Two exit codes, two meanings. 2 means a human must look at the last step. 3 means the run is too poor to continue.
Now break it on purpose. That is the only way to know it refuses.
LEDGER=tmp.jsonl FLOOR=5000 node gate.js s2; echo "exit=$?"
# expect: OK s2 remaining=9880 / exit=0
LEDGER=tmp.jsonl node -e '
require("./ledger").append({ run_id:"demo-01", step_id:"s2", intent:"draft",
evidence_ref:"doc-17", tokens:400, outcome:"unverified", reversible:true });'
LEDGER=tmp.jsonl FLOOR=5000 node gate.js s3; echo "exit=$?"
# expect: STOP: previous step unverified — s2 (unverified) / exit=2
Notice what just happened. The run stopped because a draft was unverified, not because it was wrong. You now have a named moment to review.
Stage 3 — Make the first metered call
Pull the endpoint and model name from your own project settings. Do not hardcode them, and do not guess a model name from a blog post.
export MODEL_BASE_URL="<endpoint from your project settings>"
export MODEL_NAME="<model you picked from the available list>"
export MODEL_API_KEY="<your key>"
curl -sS "$MODEL_BASE_URL" \
-H "Authorization: Bearer $MODEL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"'"$MODEL_NAME"'","messages":[{"role":"user","content":"List three sources for claim X. No summary."}]}' \
> out.json
Now count the spend yourself. If the response reports usage, trust it and label it. If it does not, estimate and say so.
node -e '
const fs = require("fs");
const res = JSON.parse(fs.readFileSync("out.json", "utf8"));
const u = res.usage || {};
const reported = Number.isFinite(u.prompt_tokens) && Number.isFinite(u.completion_tokens);
const raw = JSON.stringify(res.choices || res.messages || res);
const tokens = reported ? u.prompt_tokens + u.completion_tokens : Math.ceil(raw.length / 4);
require("./ledger").append({ run_id:"demo-01", step_id:"s3", intent:"source hunt",
evidence_ref:"doc-17", tokens, tokens_source: reported ? "reported" : "estimate",
outcome:"unverified", reversible:true });
console.log("logged", tokens, reported ? "reported" : "estimate");'
The tokens_source field matters more than the number. An estimate is fine as long as it is labelled. An unlabelled estimate is a quiet lie.
I have not run this against your endpoint. Treat it as a template you adapt, then check the new row with tail -n 1 run.jsonl.
Stage 4 — Rehearse the crash before it happens
Kill the run mid-step. Do it now, while nothing is at stake.
// replay.js — decide what is safe to redo after a crash
const ledger = require("./ledger");
const run = process.argv[process.argv.indexOf("--run") + 1];
const rows = ledger.read().filter(r => r.run_id === run);
for (const row of rows) {
const redo = row.reversible === true && row.outcome !== "verified";
console.log(`${row.step_id}\t${row.outcome}\t${redo ? "redo-safe" : "keep-as-is"}`);
}
const last = rows[rows.length - 1];
if (!last || last.outcome !== "verified") {
console.error("RESUME BLOCKED: last step unverified — hand back to a human");
process.exit(2);
}
LEDGER=tmp.jsonl node replay.js --run demo-01; echo "exit=$?"
You should see s2 marked redo-safe, s3 blocked, and a 2. The block is the point.
Auto-retry on an unverified step is how drafts get written twice. Recovery is a review decision, not a loop.
Stage 5 — Render an approval card people can actually read
Now the interface. The card must carry the ledger's four facts in words, not colour.
<section role="group" aria-labelledby="card-h" aria-describedby="card-budget card-stop card-undo">
<h2 id="card-h">Approve step 3: source hunt</h2>
<p id="card-budget">Recorded budget left: 9,480 tokens. This step is capped at 900.</p>
<p id="card-stop">Stops if the previous step is unverified, or if budget drops below 5,000.</p>
<p id="card-undo">Undo: writes to <code>drafts/</code> only. Nothing is sent.</p>
<p role="status" aria-live="polite" hidden>Cannot approve: previous step is unverified.</p>
<button type="button">Approve step 3</button>
<button type="button">Hand back to me</button>
</section>
Run four checks and write down the results. Tab through with no mouse. Confirm the refusal message is announced, not just shown. Confirm the stop condition survives at 200% zoom. Confirm refusal text still reads when colour is removed.
If any check fails, the card is not ready. The ledger is fine. The card is the problem.
Which missing evidence should stop you
This is the table I keep next to the gate.
| Missing | Stop approval? | Why |
|---|---|---|
| Which step wrote the last artifact | Yes | recovery becomes guesswork |
| Remaining recorded budget | Yes | you cannot size the run |
| Whether the next step sends or overwrites | Yes | no undo means no default |
| A polished summary of step 2 | No | noise, not a decision input |
| The model's self-reported confidence | No | it is not evidence about the world |
That last row is the one people argue with me about. Ask yourself what you would do differently with a confidence score. Usually the answer is nothing.
What I can defend, and what I cannot
I can defend the failure I hit. A five-step run broke and nobody could account for steps one through three.
I can defend the checks above. They pass, fail loudly, and produce a row you can read out loud.
The claim that pre-call ledgers reduce unrecoverable runs across teams is a hypothesis. I have not measured it across teams, and I will not pretend otherwise.
Who should not use this
Skip it if you need durable audit storage with retention rules. A JSONL file on a laptop is not compliance.
Skip it if the run executes on a machine you do not control. A gate you cannot enforce is decoration.
Skip it if you need a guaranteed service level. A free tier is a starting point, not an SLA.
Also skip it if nobody on the team will read the ledger. A record nobody reads is just a slower silence.
If you want to try the gate on a scratch run, start on the free server option and keep the ledger local. Then tell me which missing field stopped you first.
That answer is the most useful thing you can send me.
Top comments (0)